本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布
看完本文你能學(xué)到什么:
1萄唇、ShareElement是什么以及基本用法
2、理解ShareElement是如何運(yùn)作的
3诸狭、掌握ShareElement的進(jìn)階用法(Fresco、Glide、RecyclerView&ViewPager圖片視頻混合的情況下如何實(shí)現(xiàn)ShareElement動畫)
4戏溺、一個封裝好可以簡單實(shí)現(xiàn)以上ShareElement動畫的開源庫 YcShareElement(https://github.com/yellowcath/YcShareElement)
[TOC]
什么是ShareElement
ShareElement即兩個Activity(或Fragment)之間切換時的共享元素渣蜗,如下圖,可以看到旷祸,選中的聯(lián)系人頭像和名字直接很自然地過渡到了下一頁的位置耕拷,這兩個就是本次切換動畫的ShareElement
ShareElement這一套也能實(shí)現(xiàn)同一個Activity(Fragment)內(nèi)部的復(fù)雜切換動畫,不過因?yàn)樵贏ctivity內(nèi)部做動畫有太多現(xiàn)成的手段托享,所以本文不涉及這方面內(nèi)容
ShareElement應(yīng)用場景
以我個人的觀點(diǎn)骚烧,ShareElement最好的應(yīng)用場景之一就是現(xiàn)在的以圖片、視頻為主的內(nèi)容流APP嫌吠。下面是我司應(yīng)用了ShareElement的app與某app的用戶瀏覽體驗(yàn)對比
如何實(shí)現(xiàn)ShareElement
或許很多人第一次看到類似這種MaterialDesign里炫酷的界面切換效果時止潘,也會有和我一樣的疑惑,
這么炫酷的效果是怎么實(shí)現(xiàn)的辫诅?兩個Activity之間怎么能切換的如此自然凭戴?
實(shí)際上,這樣的效果單憑開發(fā)者自己確實(shí)很難實(shí)現(xiàn)炕矮,幸運(yùn)的是么夫,在Api21之后,官方提供了一套現(xiàn)成的工具來幫我們實(shí)現(xiàn)這個功能肤视,核心就是以下四個函數(shù):
Window.setEnterTransition()
Window.setExitTransition()
Window.setSharedElementEnterTransition()
Window.setSharedElementExitTransition()
這里我們先以一個簡單的仿官方聯(lián)系人效果的Demo介紹下實(shí)現(xiàn)ShareElement的基本流程
Activity A
public class ContactsActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
/**
*1档痪、打開FEATURE_CONTENT_TRANSITIONS開關(guān)(可選),這個開關(guān)默認(rèn)是打開的
*/
requestWindowFeature(Window.FEATURE_CONTENT_TRANSITIONS);
/**
*2邢滑、設(shè)置除ShareElement外其它View的退出方式(左邊滑出)
*/
getWindow().setExitTransition(new Slide(Gravity.LEFT));
super.onCreate(savedInstanceState);
...
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
...
/**
*3腐螟、設(shè)置兩個Activity的共享元素的TransitionName,
*兩個Activity的共享元素必須設(shè)置同樣的TransitionName
*/
ViewCompat.setTransitionName(avatarImg,"avatar:"+item.name);
ViewCompat.setTransitionName(nameTxt,"name:"+item.name);
}
private void gotoDetailActivity(Contacts contacts, final View avatarImg, final View nameTxt) {
Intent intent = new Intent(ContactActivity.this,DetailActivity.class);
Pair<View,String> pair1 = new Pair<>((View)avatarImg,ViewCompat.getTransitionName(avatarImg));
Pair<View,String> pair2 = new Pair<>((View)nameTxt,ViewCompat.getTransitionName(nameTxt));
/**
*4困后、生成帶有共享元素的Bundle乐纸,這樣系統(tǒng)才會知道這幾個元素需要做動畫
*/
ActivityOptionsCompat activityOptionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(ContactActivity.this, pair1, pair2);
ActivityCompat.startActivity(ContactActivity.this,intent,activityOptionsCompat.toBundle());
}
}
Activity B
public class DetailActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
ImageView avatarImg = findViewById(R.id.avatar);
TextView nameTxt = findViewById(R.id.name);
Contacts item = getIntent().getParcelableExtra(ContactsActivity.KEY_CONTACTS);
/**
* 1、設(shè)置相同的TransitionName
*/
ViewCompat.setTransitionName(avatarImg,"avatar:"+item.name);
ViewCompat.setTransitionName(nameTxt,"name:"+item.name);
/**
* 2摇予、設(shè)置WindowTransition,除指定的ShareElement外汽绢,其它所有View都會執(zhí)行這個Transition動畫
*/
getWindow().setEnterTransition(new Fade());
getWindow().setExitTransition(new Fade());
/**
* 3、設(shè)置ShareElementTransition,指定的ShareElement會執(zhí)行這個Transiton動畫
*/
TransitionSet transitionSet = new TransitionSet();
transitionSet.addTransition(new ChangeBounds());
transitionSet.addTransition(new ChangeTransform());
transitionSet.addTarget(avatarImg);
transitionSet.addTarget(nameTxt);
getWindow().setSharedElementEnterTransition(transitionSet);
getWindow().setSharedElementExitTransition(transitionSet);
}
}
運(yùn)行一下看效果
可以看到侧戴,頭像和名字位置是很順利的過渡了宁昭,但是名字的大小和顏色并沒有和之前的官方demo一樣完美過渡,這是因?yàn)楣俜侥J(rèn)提供的Transition動畫只有以下幾個:
ChangeBounds:View的大小與位置動畫
ChangeTransform:View的縮放與旋轉(zhuǎn)動畫
ChangeClipBounds:View的裁剪區(qū)域(View.getClipBounds())動畫
ChangeScroll:處理View的scrollX與scrollY屬性
ChangeImageTransform:處理ImageView的ScaleType屬性(這個在實(shí)際項(xiàng)目中有網(wǎng)絡(luò)圖片時不好用酗宋,后文有解決方案)
可以看到并沒有對TextView的字體大小和顏色做處理
俗話說得好积仗,自己動手豐衣足食,我們來自定義一個Transition動畫
public class ChangeTextTransition extends Transition {
@Override
public void captureStartValues(TransitionValues transitionValues) {}
@Override
public void captureEndValues(TransitionValues transitionValues) {}
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues){
return super.createAnimator(sceneRoot, startValues, endValues);
}
}
Transition的設(shè)計(jì)思路是蜕猫,每一個Transition類負(fù)責(zé)整個動畫的一部分斥扛,在這個例子里,TextView的平移和大小變化已經(jīng)由ChangeBounds實(shí)現(xiàn)了,因此我們自定義的Transition只需要實(shí)現(xiàn)字體大小和顏色的動畫就行了
可以看到稀颁,自定義Transition需要實(shí)現(xiàn)三個函數(shù)芬失,要達(dá)到我們想要的效果,需要:
1匾灶、在captureStartValues里獲取到TextView在Activity A里的狀態(tài)(字體和顏色)
2棱烂、在captureEndValues里獲取到TextView在Activity B里的狀態(tài)(字體和顏色)
3、在createAnimator里利用獲取到的初始和結(jié)束狀態(tài)創(chuàng)建一個Animator
最簡單的方法就是在創(chuàng)建ChangeTextTransition的時候傳入相應(yīng)的參數(shù)阶女,不過缺點(diǎn)是:
1颊糜、進(jìn)入和退出時需要不同的參數(shù)
2、如果有多個TextView都需要做動畫怎么辦秃踩?有多少傳多少參數(shù)衬鱼?
3、不夠優(yōu)雅 :)
想要解決以上缺點(diǎn)憔杨,就需要了解ShareElement動畫的完整流程
ShareElement完整流程
要實(shí)現(xiàn)自定義的ShareElement動畫鸟赫,一切的重點(diǎn)都在于Activity對外暴露的回調(diào)SharedElementCallback
SharedElementCallback
你可以通過以下兩個函數(shù)設(shè)置這個回調(diào)
activity.setExitSharedElementCallback(callback)
activity.setEnterSharedElementCallback(callback)
SharedElementCallback有以下7個回調(diào),最麻煩的是消别,這幾個回調(diào)在進(jìn)入和退出時的調(diào)用順序是不一致的
SharedElementCallback是一個抽象類抛蚤,所有回調(diào)都有默認(rèn)實(shí)現(xiàn)
/**
*最先調(diào)用,用于動畫開始前替換ShareElements寻狂,比如在Activity B翻過若干頁大圖之后岁经,返回Activity A
*的時候需要縮小回到對應(yīng)的小圖,就需要在這里進(jìn)行替換
*/
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {}
/**
*表示ShareElement已經(jīng)全部就位蛇券,可以開始動畫了
*/
public void onSharedElementsArrived(List<String> sharedElementNames, List<View> sharedElements, OnSharedElementsReadyListener listener) {}
/**
*在之前的步驟里(onMapSharedElements)被從ShareElements列表里除掉的View會在此回調(diào)缀壤,
*不處理的話默認(rèn)進(jìn)行alpha動畫消失
*/
public void onRejectSharedElements(List<View> rejectedSharedElements) {}
/**
*在這里會把ShareElement里值得記錄的信息存到為Parcelable格式,以發(fā)送到Activity B
*默認(rèn)處理規(guī)則是ImageView會特殊記錄Bitmap纠亚、ScaleType诉位、Matrix,其它View只記錄大小和位置
*/
public Parcelable onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix, RectF screenBounds) {}
/**
*在這里會把Activity A傳過來的Parcelable數(shù)據(jù)菜枷,重新生成一個View,這個View的大小和位置會與Activity A里的
*ShareElement一致叁丧,
*/
public View onCreateSnapshotView(Context context, Parcelable snapshot) {}
public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {}
public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {}
下圖展示了從Activity A切換到Activity B啤誊,SharedElementCallback被調(diào)用的時序
查看原圖
圖里我標(biāo)了幾個值得注意的點(diǎn):
1、moveSharedElementsToOverlay()
protected void moveSharedElementsToOverlay() {
...
ViewGroup decor = getDecor();
if (decor != null) {
...
for (int i = 0; i < numSharedElements; i++) {
View view = mSharedElements.get(i);
if (view.isAttachedToWindow()) {
...
GhostView.addGhost(view, decor, tempMatrix);
...
}
}
}
}
ViewOverlay在Android4.3加入拥娄,其父類是ViewGroup,如果想在一個View最上層展示一些東西蚊锹,可以調(diào)用View.getOverlay(),然后調(diào)用ViewOverlay.add(drawable)或者ViewOverlay.getOverlayView().addView()函數(shù)添加到ViewOverlay.
GhostView可以在不改變一個View的Parent的情況下,把View渲染到另一個ViewGroup里面去.
moveSharedElementsToOverlay()函數(shù)實(shí)質(zhì)就是把ShareElementView渲染到整個Activity的最上層(DecorView的ViewOverlay)稚瘾,
這樣在做動畫時ShareElementView就不會被任何別的東西遮擋住.
2牡昆、setSharedElementState()
這里需要提一點(diǎn),在這個Demo里,整個ShareElement動畫過程中丢烘,做動畫的都只有Activity B里的ShareElement,Activity A里的ShareElement唯一的作用就是提供位置大小等參數(shù)柱宦,然后這些參數(shù)在setSharedElementState()函數(shù)里被設(shè)置到Activity B里對應(yīng)的View上.
private void setSharedElementState(View view, String name, Bundle transitionArgs,
Matrix tempMatrix, RectF tempRect, int[] decorLoc) {
...
if (view instanceof ImageView) {
...
imageView.setScaleType(scaleType);
if (scaleType == ImageView.ScaleType.MATRIX) {
float[] matrixValues = sharedElementBundle.getFloatArray(KEY_IMAGE_MATRIX);
tempMatrix.setValues(matrixValues);
imageView.setImageMatrix(tempMatrix);
}
}
....
view.setLeft(0);
view.setTop(0);
view.setRight(Math.round(width));
view.setBottom(Math.round(height));
...
view.measure(widthSpec, heightSpec);
view.layout(x, y, x + width, y + height);
}
可以看見,如果不是ImageView播瞳,系統(tǒng)只處理了大小位置的信息掸刊,這也是我們前面的動畫里為什么名字的過渡效果那么不自然,因?yàn)橄到y(tǒng)壓根就沒管字體大小和顏色之類的東西.
(如果是進(jìn)入動畫)在設(shè)置好信息之后赢乓,會先調(diào)用SharedElementCallback.onSharedElementStart忧侧,然后就是Transition.captureStartValues()
3、setOriginalSharedElementState()
protected static void setOriginalSharedElementState(ArrayList<View> sharedElements,
ArrayList<SharedElementOriginalState> originalState) {
for (int i = 0; i < originalState.size(); i++) {
View view = sharedElements.get(i);
SharedElementOriginalState state = originalState.get(i);
if (view instanceof ImageView && state.mScaleType != null) {
ImageView imageView = (ImageView) view;
imageView.setScaleType(state.mScaleType);
if (state.mScaleType == ImageView.ScaleType.MATRIX) {
imageView.setImageMatrix(state.mMatrix);
}
}
view.setElevation(state.mElevation);
view.setTranslationZ(state.mTranslationZ);
int widthSpec = View.MeasureSpec.makeMeasureSpec(state.mMeasuredWidth,
View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(state.mMeasuredHeight,
View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
view.layout(state.mLeft, state.mTop, state.mRight, state.mBottom);
}
}
在Transition.captureStartValues()之后牌芋,接著setOriginalSharedElementState()函數(shù)會恢復(fù)view在Activity B里的狀態(tài)蚓炬,
再調(diào)用Transition.captureEndValues().
這時候動畫的起始和結(jié)束狀態(tài)的已經(jīng)獲得了,TransitionManager就會在onPreDraw()的回調(diào)里執(zhí)行Transiton.playTransition(),
這里面會調(diào)用Transition.createAnimator()函數(shù)躺屁,然后執(zhí)行這個Animator.這時候ShareElement動畫就真正開始了.
返回流程
返回流程這里就不詳細(xì)分析了肯夏,直接給出各個回調(diào)的調(diào)用順序
ActivityB.onMapSharedElements()
->ActivityA.onMapSharedElements()
->ActivityA.onCaptureSharedElementSnapshot()
->ActivityB.onCreateSnapshotView()
->ActivityB.onSharedElementEnd()
->ActivityB.onSharedElementStart() //你沒有看錯,就是先End再Start
->ActivityB.onSharedElementsArrived()
->ActivityA.onSharedElementsArrived()
->ActivityA.onRejectSharedElements()
->ActivityA.onCreateSnapshotView()
->ActivityA.onSharedElementStart()
->ActivityA.onSharedElementEnd()
自定義Transition
由上面的分析可以得出楼咳,要實(shí)現(xiàn)TextView的Transition熄捍,需要以下步驟
查看原圖
實(shí)際代碼可參考ChangeTextTransition
YcShareElement
demo里用了
GSYVideoPlayer展示視頻
Fresco、Glide展示圖片
YcShareElement提供了兩個demo母怜,一個是上面的聯(lián)系人demo余耽,另一個實(shí)現(xiàn)了圖片、視頻混合的列表頁與詳情頁之間的ShareElement動畫苹熏,如下圖
這里面的關(guān)鍵點(diǎn)如下:
1碟贾、Glide圖片的ShareElement動畫
ImageView在動畫過程中要經(jīng)歷默認(rèn)背景色->小縮略圖->大圖三個階段,如何在這三個階段里做到無縫切換
參考:ChangeOnlineImageTransition
2轨域、Fresco圖片的ShareElement動畫
Fresco提供了內(nèi)置的DraweeTransition袱耽,但是如果設(shè)置了縮略圖,圖片就會變形干发,并且必須在構(gòu)造函數(shù)里提供動畫起始的ScaleType信息朱巨,簡單的情況很好用,在復(fù)雜的情況下不太友好
參考:AdvancedDraweeTransition
3枉长、從列表的Webp動圖到詳情頁的視頻ShareElement動畫
這個在實(shí)現(xiàn)了以上兩點(diǎn)之后其實(shí)就很簡單了,實(shí)際上就是視頻的封面圖做動畫
普通頁面使用步驟
1冀续、打開WindowContentTransition開關(guān)
YcShareElement.enableContentTransition(getApplication());
由于這個開關(guān)默認(rèn)是打開的,因此這一句是可選的必峰,擔(dān)心遇到奇葩手機(jī)關(guān)掉這個開關(guān)的可以調(diào)用
2洪唐、生成Bundle,然后startActivity
private void gotoDetailActivity(){
Intent intent = new Intent(this, DetailActivity.class);
Bundle bundle = YcShareElement.buildOptionsBundle(ContactActivity.this, new IShareElements() {
@Override
public ShareElementInfo[] getShareElements() {
return new ShareElementInfo[]{new ShareElementInfo(mAvatarImg),
new ShareElementInfo(mNameTxt, new TextViewStateSaver())};
}
});
ActivityCompat.startActivity(ContactActivity.this, intent, bundle);
}
3吼蚁、新的頁面里設(shè)置并啟動Transition
public class DetailActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
YcShareElement.setEnterTransition(this, new IShareElements() {
@Override
public ShareElementInfo[] getShareElements() {
return new ShareElementInfo[]{new ShareElementInfo(avatarImg),
new ShareElementInfo(nameTxt, new TextViewStateSaver())};
}
});
YcShareElement.startTransition(this);
}
}
YcShareElement.setEnterTransition()默認(rèn)會暫停Activity的Transtion動畫凭需,直到調(diào)用YcShareElement.startTransition(),
在這種不需要等待ShareElement加載的簡單頁面,可以將第三個參數(shù)傳false,就不會暫停ActivityB的Transition動畫了,如下
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
YcShareElement.setEnterTransition(this, new IShareElements() {
@Override
public ShareElementInfo[] getShareElements() {
return new ShareElementInfo[]{new ShareElementInfo(avatarImg),
new ShareElementInfo(nameTxt, new TextViewStateSaver())};
}
},false);
}
效果如下:
圖片&視頻頁面使用步驟
1粒蜈、打開WindowContentTransition開關(guān)
YcShareElement.enableContentTransition(getApplication());
2顺献、生成Bundle,然后startActivity
Bundle options = YcShareElement.buildOptionsBundle(getActivity(), this);
startActivityForResult(intent, REQUEST_CONTENT, options);
3薪伏、Activity B設(shè)置Transtion動畫
protected void onCreate(@Nullable Bundle savedInstanceState) {
YcShareElement.setEnterTransition(this, this);
...
}
4滚澜、Activity B的ViewPager加載好之后啟動Transition
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
...加載數(shù)據(jù)...
YcShareElement.postStartTransition(getActivity());
}
這時候進(jìn)入動畫就執(zhí)行完畢了,接下來要處理滑動若干頁之后返回列表頁的情況
5嫁怀、Activity B實(shí)現(xiàn)finishAfterTransition()函數(shù)
@Override
public void finishAfterTransition() {
YcShareElement.finishAfterTransition(this, this);
super.finishAfterTransition();
}
6设捐、Activity A實(shí)現(xiàn)onActivityReenter()函數(shù)
@Override
public void onActivityReenter(int resultCode, Intent data) {
super.onActivityReenter(resultCode, data);
YcShareElement.onActivityReenter(this, resultCode, data, new IShareElementSelector() {
@Override
public void selectShareElements(List<ShareElementInfo> list) {
//將列表頁滑動到變更后的ShareElement的位置
mFragment.selectShareElement(list.get(0));
}
});
}
如何擴(kuò)展支持自定義View的Transition動畫
這里以Fresco為例介紹如何進(jìn)行擴(kuò)展
1、確定所需參數(shù)
首先確定SimpleDraweeView做Transtion動畫需要的參數(shù)塘淑,即ActualImageScaleType
2萝招、繼承ViewStateSaver,獲取所需參數(shù)
public class FrescoViewStateSaver extends ViewStateSaver {
@Override
protected void captureViewInfo(View view, Bundle bundle) {
if (view instanceof GenericDraweeView) {
int actualScaleTypeInt = scaleTypeToInt(((GenericDraweeView)view).getHierarchy().getActualImageScaleType())
bundle.putInt("scaleType",actualScaleTypeInt);
}
}
public ScalingUtils.ScaleType getScaleType(Bundle bundle) {
int scaleType = bundle.getInt("scaleType", 0);
return intToScaleType(scaleType);
}
}
3存捺、自定義Transition
public class AdvancedDraweeTransition extends Transition {
private ScalingUtils.ScaleType mFromScale;
private ScalingUtils.ScaleType mToScale;
public AdvancedDraweeTransition() {
addTarget(GenericDraweeView.class);
}
@Override
public void captureStartValues(TransitionValues transitionValues) {
...
ShareElementInfo shareElementInfo = ShareElementInfo.getFromView(transitionValues.view);
mFromScale = ((FrescoViewStateSaver) shareElementInfo.getViewStateSaver()).getScaleType(viewInfo);
...
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
...
ShareElementInfo shareElementInfo = ShareElementInfo.getFromView(transitionValues.view);
mToScale = ((FrescoViewStateSaver) shareElementInfo.getViewStateSaver()).getScaleType(viewInfo);
...
}
@Override
public Animator createAnimator(
ViewGroup sceneRoot,
TransitionValues startValues,
TransitionValues endValues) {
..
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = (float) animation.getAnimatedValue();
scaleType.setValue(fraction);
if (draweeView.getHierarchy().getActualImageScaleType() != scaleType) {
draweeView.getHierarchy().setActualImageScaleType(scaleType);
}
}
});
...
return animator;
}
}
4槐沼、使用自定義的Transition
public class FrescoShareElementTransitionfactory extends DefaultShareElementTransitionFactory {
@Override
protected TransitionSet buildShareElementsTransition(List<View> shareViewList) {
TransitionSet transitionSet = super.buildShareElementsTransition(shareViewList);
transitionSet.addTransition(new AdvancedDraweeTransition());
return transitionSet;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
YcShareElement.setEnterTransitions(this, this,true,new FrescoShareElementTransitionfactory());
...
}
廣告時間
在文末安利一下我的另外幾個開源庫,歡迎大家來提issue捌治、star岗钩、fork
PhotoMovie:高仿抖音照片電影功能
VideoProcessor:用硬編碼實(shí)現(xiàn)視頻的快慢放、倒流及混音功能
SVideoRecorder:硬編碼短視頻錄制肖油,支持分段錄制兼吓、所見即所得