Android共享元素轉場動畫兼容實踐

原文地址

Android Shared-Element Transitions for all

我們都希望我們的app有自己特殊的地方,轉場動畫就是一個比較好的方式讓用戶記住我們的應用。在Lollipop+ 上的版本實現起來十分的簡單,但是如果想兼容低于5.0的版本,你或許需要檢查Android系統的版本來做一些功能上的削減,或者你可以勇敢的手動來實現這個轉換,瘋狂的想法,但是我們可以來這么嘗試一下。

共享元素變換步驟

當你想要從一個Activity A轉換到Activity B,而且他們共享一個元素(比如是一個view),在這種場景下,最好的用戶體驗可能就是將共享的元素直接變換到最終的地方和大小,這會使用戶專注于應用而且有一種連貫性的表達。那么怎么實現這樣的過渡呢?可能需要一些步驟

  • Activity A解析共享元素的開始值然后通過intent傳遞給Activity B
  • Activity B開始的時候是完全透明的
  • Activity B從Bundle里取出值并準備場景
  • Activity B開始做共享元素變換的動畫

我將會在這篇文章中展示如何實現這些步驟的細節和一些示例代碼。首先,先進行命名,我們將在 Activity A中的共享view稱之為origin view(初始視圖),并將Activity B中的共享view稱之為destination view(目標視圖)。雖然這兩個view被稱之為共享view,但實際上他們只是碰巧有相同內容的完全不相關的view。

Activity A解析開始值并傳遞給Activity B

當我們想要創建一個從Activity A到Activity B轉換的視覺過渡效果,第一步就是解析出兩個Activity中的共享視圖內開始和結束值這樣,后期需要這個數據來做變換。
在Activity B中我們只能解析后目標視圖的屬性,對于初始視圖的屬性需要通過intent傳遞過來

        Intent intent = new Intent(context, ArticleImageActivity.class);
        intent.putExtra(IMAGE_URL_EXTRA, imageUrl);
        intent.putExtra(VIEW_INFO_EXTRA, /* start values */ captureValues(originView));

        startActivity(intent);
        overridePendingTransition(0, 0);

為了去掉默認的轉場效果,我們需要在 startActivity 后面調用一次 overridePendingTransition(0,0) ,然后就能實現我們自己的效果。
這個 captureValues(View) 的方法會將view的大小和位置打包成一個bundle返回出來

    private Bundle captureValues(@NonNull View view) {
        Bundle b = new Bundle();
        int[] screenLocation = new int[2];
        view.getLocationOnScreen(screenLocation);
        b.putInt(PROPNAME_SCREENLOCATION_LEFT, screenLocation[0]);
        b.putInt(PROPNAME_SCREENLOCATION_TOP, screenLocation[1]);
        b.putInt(PROPNAME_WIDTH, view.getWidth());
        b.putInt(PROPNAME_HEIGHT, view.getHeight());
        return b;
    }

Activity B設置背景透明

在startActivity之后就跳轉到了Activity B中,但是我們不希望用戶一開始就能看到整個布局直到我們的過渡動畫準備好開始運動。解決方法十分的簡單,就是保證這個activity一開始是透明的,而且將layout都設置為 INVISIBLE

    <style name="Transparent" parent="AppTheme">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
    </style>

Activity B場景布置

這一步是最為重要的,首先,確保所有的資源文件準備完畢,假設這個共享view是一個 ImageView 而且圖片已經從URL中加載出來了。這個并不難可以使用比如 Picasso, Glide, 或者其他的library。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_article_image);
        ...
        // start the information passed in the Bundle
        extractViewInfoFromBundle(getIntent());
        // only now we load the image
        Picasso.load(mImageUrl)
                .into(mDestinationView, new Callback() {
                    @Override
                    public void onSuccess() {
                        // we've got the image loaded, we can start prepping the scene
                        onUiReady();
                    }
                    @Override
                    public void onError() {...}
                });
        ...
    }

這個 onUiReady() 方法會在所有的資源獲取之后準備界面以及過渡動畫的實現,現在已經通過intent傳遞過來了初始視圖的屬性值,然后我們需要拿到目標視圖的屬性值,所以需要一個在view被layout之后還沒有draw之前的時機,官方給我們提供了一個很好的回調方式 onPreDraw()

    private void onUiReady() {
        mDestinationView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                // remove previous listener
                mDestinationView.getViewTreeObserver().removeOnPreDrawListener(this);
                // prep the scene
                prepareScene();
                // run the animation
                runEnterAnimation();
                return true;
            }
        });
    }

prepareScene() 中我們需要拿到目標視圖的屬性,并且根據在屏幕上的位置和大小做一個差值的屬性轉換

    private void prepareScene() {
        // capture the end values in the destionation view
        mEndValues = captureValues(mDestinationView);

        // calculate the scale and positoin deltas
        float scaleX = scaleDelta(mStartValues, mEndValues);
        float scaleY = scaleDelta(mStartValues, mEndValues);
        int deltaX = translationDelta(mStartValues, mEndValues);
        int deltaY = translationDelta(mStartValues, mEndValues);

        // scale and reposition the image
        mDestinationView.setScaleX(scaleX);
        mDestinationView.setScaleY(scaleY);
        mDestinationView.setTranslationX(deltaX);
        mDestinationView.setTranslationY(deltaY);
    }

準備好之后就是設置過渡動畫了

Activity B過渡動畫

目標視圖現在和初始視圖在同樣的地方大小也一樣,然后我們做動畫配合千米那設置的屬性變換來讓它移動到最后的位置,并且縮放到適當的大小

    private void runEnterAnimation() {
        // We can now make it visible
        mDestinationView.setVisibility(View.VISIBLE);
        // finally, run the animation
        mDestinationView.animate()
                .setDuration(DEFAULT_DURATION)
                .setInterpolator(DEFAULT_INTERPOLATOR)
                .scaleX(1f)
                .scaleY(1f)
                .translationX(0)
                .translationY(0)
                .start();
    }

在我們需要從Activity B返回到Activity A,只需要再做一個反轉的動畫,你可以在 onBackPressed() 的回調或者是其他會返回到前面的Activity的地方調用下面這段代碼。

    private void runExitAnimation() {
        mDestinationView.animate()
                .setDuration(DEFAULT_DURATION)
                .setInterpolator(DEFAULT_INTERPOLATOR)
                .scaleX(scaleX)
                .scaleY(scaleY)
                .translationX(deltaX)
                .translationY(deltaY)
                .withEndAction(new Runnable() {
                    @Override
                    public void run() {
                        finish();
                        overridePendingTransition(0, 0);
                    }
                }).start();
    }

到這里整個步驟就結束了,而且這種方式能兼容所有的Android版本,不限于5.0以上,下面的采取這種方式實踐的效果。

效果演示

備注

推薦一個圖片手勢操作的library,其中包含了這個共享元素的使用,可以看一下具體的實現方式

https://github.com/alexvasilkov/GestureViews

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容