Android - 如何从listview中的项目创建一个到整个活动的过渡?

28

我的要求是当用户在ListView中点击列表项时,它会转换为整个活动(如下面的示例所示),但我无法找到解释此过程的教程,实际上,我不知道这个过程叫什么。

换句话说,我想要实现以下内容:

  1. 当被点击时,增加列表项的高程(如右边的gif中所示)

  2. 展开并将列表项转换为包含有关所点击项的详细信息的下一个片段/活动布局

输入图像描述

我尝试了很多过渡效果,但都没有成功。 有人能帮我完成这个吗?


你考虑过使用 ActivityOptions.makeSceneTransitionAnimation 吗? - Sir4ur0n
实际上,我正在使用以下代码:ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this, view, getString(R.string.transition_name)); - Antonio
你可以在这里找到有关转换的信息:https://developer.android.com/training/material/animations.html#Transitions - Prakash
嘿 @Antonio,如果你已经完成了这个任务,能否分享一下你的代码或者任何实现这个任务的样例? - Manu
4个回答

18

我制作了一个小示例应用程序,可以在两个活动之间进行所需的效果转换:

样例应用

但是所提供的gif中的转换略有不同。左侧gif中的转换将列表元素转换为第二个活动的内容区域(工具栏保持原位)。右侧gif中的转换将列表元素转换为第二个活动的完整屏幕。以下代码提供了左侧gif中的效果。但是通过轻微修改应该可以适应右侧gif中的转换。

请注意,这仅适用于Lollipop。但是可以模拟旧设备上的不同效果。此外,所提供的代码的唯一目的是展示如何实现它。请勿直接在您的应用程序中使用此代码。

MainActivity:

public class MainActivity extends AppCompatActivity {

    MyAdapter myAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
        ListView listView = (ListView) findViewById(R.id.list_view);

        myAdapter = new MyAdapter(this, 0, DataSet.get());

        listView.setAdapter(myAdapter);

        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, final View view, final int position, long id) {
                startTransition(view, myAdapter.getItem(position));
            }
        });
    }

    private void startTransition(View view, Element element) {
        Intent i = new Intent(MainActivity.this, DetailActivity.class);
        i.putExtra("ITEM_ID", element.getId());

        Pair<View, String>[] transitionPairs = new Pair[4];
        transitionPairs[0] = Pair.create(findViewById(R.id.toolbar), "toolbar"); // Transition the Toolbar
        transitionPairs[1] = Pair.create(view, "content_area"); // Transition the content_area (This will be the content area on the detail screen)

        // We also want to transition the status and navigation bar barckground. Otherwise they will flicker
        transitionPairs[2] = Pair.create(findViewById(android.R.id.statusBarBackground), Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME);
        transitionPairs[3] = Pair.create(findViewById(android.R.id.navigationBarBackground), Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME);
        Bundle b = ActivityOptionsCompat.makeSceneTransitionAnimation(MainActivity.this, transitionPairs).toBundle();

        ActivityCompat.startActivity(MainActivity.this, i, b);
    }
}

activity_main.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        android:transitionName="toolbar" />

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

详细信息活动:

public class DetailActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);

        setSupportActionBar((Toolbar) findViewById(R.id.toolbar));

        long elementId = getIntent().getLongExtra("ITEM_ID", -1);
        Element element = DataSet.find(elementId);


        ((TextView) findViewById(R.id.title)).setText(element.getTitle());
        ((TextView) findViewById(R.id.description)).setText(element.getDescription());

        // if we transition the status and navigation bar we have to wait till everything is available
        TransitionHelper.fixSharedElementTransitionForStatusAndNavigationBar(this);
        // set a custom shared element enter transition
        TransitionHelper.setSharedElementEnterTransition(this, R.transition.detail_activity_shared_element_enter_transition);
    }
}

活动详情页的XML布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        android:transitionName="toolbar" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#abc"
        android:orientation="vertical"
        android:paddingBottom="200dp"
        android:transitionName="content_area"
        android:elevation="10dp">

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>

detail_activity_shared_element_enter_transition.xml (/res/transition/):

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="together">
    <changeBounds/>
    <changeTransform/>
    <changeClipBounds/>
    <changeImageTransform/>
    <transition class="my.application.transitions.ElevationTransition"/>
</transitionSet>

我的应用程序转换.ElevationTransition:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class ElevationTransition extends Transition {

    private static final String PROPNAME_ELEVATION = "my.elevation:transition:elevation";

    public ElevationTransition() {
    }

    public ElevationTransition(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    private void captureValues(TransitionValues transitionValues) {
        Float elevation = transitionValues.view.getElevation();
        transitionValues.values.put(PROPNAME_ELEVATION, elevation);
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
        if (startValues == null || endValues == null) {
            return null;
        }

        Float startVal = (Float) startValues.values.get(PROPNAME_ELEVATION);
        Float endVal = (Float) endValues.values.get(PROPNAME_ELEVATION);
        if (startVal == null || endVal == null || startVal.floatValue() == endVal.floatValue()) {
            return null;
        }

        final View view = endValues.view;
        ValueAnimator a = ValueAnimator.ofFloat(startVal, endVal);
        a.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                view.setElevation((float)animation.getAnimatedValue());
            }
        });

        return a;
    }
}

TransitionHelper:

public class TransitionHelper {

    public static void fixSharedElementTransitionForStatusAndNavigationBar(final Activity activity) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
            return;

        final View decor = activity.getWindow().getDecorView();
        if (decor == null)
            return;
        activity.postponeEnterTransition();
        decor.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @TargetApi(Build.VERSION_CODES.LOLLIPOP)
            @Override
            public boolean onPreDraw() {
                decor.getViewTreeObserver().removeOnPreDrawListener(this);
                activity.startPostponedEnterTransition();
                return true;
            }
        });
    }

    public static void setSharedElementEnterTransition(final Activity activity, int transition) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
            return;
        activity.getWindow().setSharedElementEnterTransition(TransitionInflater.from(activity).inflateTransition(transition));
    }
}

那么这里有哪些不同的部分呢: 我们有两个活动。在过渡期间,四个视图在这些活动之间进行了过渡。

  • 工具栏(Toolbar):像左侧gif中一样,工具栏不随其他内容一起移动。

  • ListView元素视图->变为DetailActivity的内容视图

  • StatusBar和NavigationBar背景:如果我们不将这些视图添加到过渡视图集中,它们将在转换期间淡入淡出。但是这需要延迟进入转换(请参见:TransitionHelper.fixSharedElementTransitionForStatusAndNavigationBar

MainActivity中,过渡视图被添加到用于启动DetailActivity的Bundle中。此外,这些过渡视图需要在两个活动中命名(transitionName)。这可以在布局xml中以及以编程方式完成。

默认的过渡集会影响视图的不同方面(例如:视图边界-请参见2)。但是,视图高度的差异不会被动画化。这就是为什么本解决方案利用了自定义的ElevationTransition的原因。


1
惊人的答案。你赢得了悬赏!恭喜。我只想添加一条评论:我在使用findViewById(android.R.id.navigationBarBackground)时遇到了一些问题,因为它是空的。我解决的方法是:将以下代码添加到转换xml中 <targets> <target android:excludeId="@android:id/statusBarBackground"/> <target android:excludeId="@android:id/navigationBarBackground"/> </targets> - Antonio
1
很高兴听到答案有所帮助。这样做是否会在转换期间导致状态栏和导航栏闪烁?另一个解决方案可能是仅在这些视图不为空时将它们添加到包中。我可以想象navigationBarBackground会变成null,因为它并不适用于所有设备? - Andreas Wenger
实际上,你是对的。我没有注意到在转换期间导航栏会闪烁。我会尝试找出为什么会发生这种情况。 - Antonio
已解决,@Andreas Wenger!这是由于我正在运行另一个转换。现在它像魅力一样工作!谢谢 - Antonio
在代码中使用Element需要导入什么? - Sourav Roy
@SouravRoy Element只是列表中不同项的数据传输对象(DTO)。在此示例中,每个元素都有一个id、标题和描述。静态的DataSet提供了可用的Elements。但是对于真实应用程序,DataSet可能是一个数据库,而Element则是该数据库中的行。 - Andreas Wenger

6

try this.. Material-Animations

blueIconImageView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent i = new Intent(MainActivity.this, SharedElementActivity.class);

        View sharedView = blueIconImageView;
        String transitionName = getString(R.string.blue_name);

        ActivityOptions transitionActivityOptions = ActivityOptions.makeSceneTransitionAnimation(MainActivity.this, sharedView, transitionName);
        startActivity(i, transitionActivityOptions.toBundle());
    }
});

SharedElements


谢谢您的回答,Carlos。但是我想实现的正是示例中的过渡效果,其中列表项变为活动。我尝试为我的列表项和最外层父项设置相同的transitionName,但没有成功。 - Antonio
你应该查看源代码示例并查看有何不同... https://github.com/lgvalle/Material-Animations/archive/master.zip - Carlos.V
我已经添加了一个gif示例,准确地展示了我的需求。感谢您的回复。这真的很有趣,但我看不到我想要做的事情。 - Antonio

0
你需要的动画被称为共享元素之间的活动转换。 通过研究,我发现你应该:
  1. 将您的ListView视图放在RelativeLayout中
  2. 在OnClick事件中,充气一个渲染器的副本
  3. 查找渲染器相对于ListView父级的全局坐标
  4. 将复制的渲染器添加到RelativeLayout(即ListView的父级)中
  5. 使listView消失动画
  6. 在该动画结束时,动画化您的新渲染器
  7. 利润!

     public class MainActivity extends Activity {
    
     private RelativeLayout layout;
            private ListView listView;
            private MyRenderer selectedRenderer;
    
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                layout = new RelativeLayout(this);
                setContentView(layout);
                listView = new ListView(this);
                RelativeLayout.LayoutParams rlp = new RelativeLayout.LayoutParams(
                        RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
                layout.addView(listView, rlp);
    
                listView.setAdapter(new MyAdapter());
                listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                    @Override
                    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    
                        // 找出单击视图相对于父容器的位置
                        int t = view.getTop() + listView.getTop();
                        int l = view.getLeft() + listView.getLeft();
    
                        // 创建ListView的副本并将其添加到父容器中
                        // 在与listview相同的位置
                        selectedRenderer = new MyRenderer(view.getContext());
                        RelativeLayout.LayoutParams rlp = new RelativeLayout.LayoutParams(view.getWidth(), view
                                .getHeight());
                        rlp.topMargin = t;
                        rlp.leftMargin = l;
                        selectedRenderer.textView.setText(((MyRenderer) view).textView.getText());
                        layout.addView(selectedRenderer, rlp);
                        view.setVisibility(View.INVISIBLE);
    
                        // 动画化listView
                        Animation outAni = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f,
                                Animation.RELATIVE_TO_SELF, -1f, Animation.RELATIVE_TO_SELF, 0f,
                                Animation.RELATIVE_TO_SELF, 0f);
                        outAni.setDuration(1000);
                        outAni.setFillAfter(true);
                        outAni.setAnimationListener(new Animation.AnimationListener() {
                            @Override
                            public void onAnimationStart(Animation animation) {
                            }
    
                            @Override
                            public void onAnimationRepeat(Animation animation) {
                            }
    
                            @Override
                            public void onAnimationEnd(Animation animation) {
                                ScaleAnimation scaleAni = new ScaleAnimation(1f, 
                                        1f, 1f, 2f, 
                                        Animation.RELATIVE_TO_SELF, 0.5f,
                                        Animation.RELATIVE_TO_SELF, 0.5f);
                                scaleAni.setDuration(400);
                                scaleAni.setFillAfter(true);
                                selectedRenderer.startAnimation(scaleAni);
                            }
                        });
    
                        listView.startAnimation(outAni);
                    }
                });
            }
    
            public class MyAdapter extends BaseAdapter {
                @Override
                public int getCount() {
                    return 10;
                }
    
                @Override
                public String getItem(int position) {
                    return "Hello World " + position;
                }
    
                @Override
                public long getItemId(int position) {
                    return position;
                }
    
                @Override
                public View getView(int position, View convertView, ViewGroup parent) {
                    MyRenderer renderer;
                    if (convertView != null)
                        renderer = (MyRenderer) convertView;
                    else
                        renderer = new MyRenderer(MainActivity.this);
                    renderer.textView.setText(getItem(position));
                    return renderer;
                }
            }
    
            public class MyRenderer extends RelativeLayout {
    
                public TextView textView;
    
                public MyRenderer(Context context) {
                    super(context);
                    setPadding(20, 20, 20, 20);
                    setBackgroundColor(0xFFFF0000);
    
                    RelativeLayout.LayoutParams rlp = new RelativeLayout.LayoutParams(
                            RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
                    rlp.addRule(CENTER_IN_PARENT);
                    textView = new TextView(context);
                    addView(textView, rlp);
                }
    
            }        }
    

谢谢您的回答,@Vishavjeet,但在您的示例中,只是缩放了该项。但我需要一个具有工具栏、其他视图等的新窗口。 - Antonio

0

试试这个绝妙的网页 @ 入门Activity和Fragment转换(第1部分)。在这里,他们谈论了Activity和Fragment转换。我还没有尝试过。我的看法是Fragment转换更好,计算机负担更小,所以这是一个很好的开始。而且你可能不需要改变工具栏,你可以显示/隐藏它们。

另一个好的SO链接是@ 在片段之间动画转换, 看看最佳答案。在那篇文章中,他们谈到了objectAnimator

另一个观点是关于您发布的示例动画,它没有展示出从一种艺术形式到另一种艺术形式的平滑动画。当动画不流畅时,它就不那么令人印象深刻了。

祝你好运,玩得开心,保持联系。


谢谢您的回复。我已经使用共享元素进行过工作,但是我不知道如何将它们应用于我想要实现的动画中。我修改了我的帖子,并添加了更多关于我想要达到的目标的信息。 - Antonio

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接