SmartRefreshLayout 仿美團下拉重新整理
阿新 • • 發佈:2018-12-11
先上圖:
一、分析
美團的下拉載入動畫初看挺簡單的,就一個賣萌的小人。細看的話還稍微有點複雜,一共有三個狀態。
- 剛開始下拉的時候,小腦袋從小變大的過程。
- 下拉到一定程度但還沒鬆手,小人翻了個跟頭直到完全出現。再往下拉保持最後完全出現的狀態。
- 鬆開後左右搖頭賣萌直至載入結束回彈回去。
二、反編譯app看實現原理
最簡單直白的方法就是反編譯美團app,雖然看不到程式碼但資原始檔能還原出來,圖片和 xml 檔案完美還原。
大部分圖片都放在 res/drawable-xhdpi-v4
和 res/drawable-xxhdpi-v4
兩個資料夾內,
仔細找下能看到多張連續的 loading 圖片。
看到圖片後知道原來它用的是最普通的幀動畫拿到資源圖片,知道了實現原理
三、實現動畫效果
首先自定義View CustomRefreshHeader
繼承自 LinearLayout
,並實現 SmartRefreshLayout
的 RefreshHeader
介面。
然後主要就是重寫 RefreshHeader
介面中的方法,裡面提供了下拉重新整理時不同階段的回撥,找到對應的方法碼程式碼就好。
/** * @author Amarao * @date 2018/9/12 * 下拉重新整理動畫 * 呼叫舉例:smartRefresh.setRefreshHeader(new CustomRefreshHeader(this)); */ public class CustomRefreshHeader extends LinearLayout implements RefreshHeader { private ImageView mImage; private AnimationDrawable pullDownAnim; private AnimationDrawable refreshingAnim; private boolean hasSetPullDownAnim = false; public CustomRefreshHeader(Context context) { super(context,null,0); View view = View.inflate(context, R.layout.custom_refresh_header,this); mImage = (ImageView) view.findViewById(R.id.hot_iv_refresh_header); } public CustomRefreshHeader(Context context, @Nullable AttributeSet attrs) { super(context, attrs,0); } public CustomRefreshHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); View view = View.inflate(context, R.layout.custom_refresh_header,this); mImage = (ImageView) view.findViewById(R.id.hot_iv_refresh_header); LogUtils.e("CustomRefreshHeader3"); } /** * 獲取真實檢視(必須返回,不能為null) */ @NonNull @Override public View getView() { return this; } /** * 獲取變換方式(必須指定一個:平移、拉伸、固定、全屏) */ @NonNull @Override public SpinnerStyle getSpinnerStyle() { return SpinnerStyle.Translate; } /** * 設定主題顏色 (如果自定義的Header沒有注意顏色,本方法可以什麼都不處理) * @param colors 對應Xml中配置的 srlPrimaryColor srlAccentColor */ @Override public void setPrimaryColors(int... colors) { } /** * 尺寸定義初始化完成 (如果高度不改變(程式碼修改:setHeader),只調用一次, 在RefreshLayout#onMeasure中呼叫) * @param kernel RefreshKernel 核心介面(用於完成高階Header功能) * @param height HeaderHeight or FooterHeight * @param maxDragHeight 最大拖動高度 */ @Override public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) { } /** * 開始動畫(開始重新整理或者開始載入動畫) * @param refreshLayout RefreshLayout * @param height HeaderHeight or FooterHeight * @param maxDragHeight 最大拖動高度 */ @Override public void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) { } /** * 狀態改變時呼叫。在這裡切換第三階段的動畫賣萌小人 * @param refreshLayout * @param oldState * @param newState */ @Override public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) { switch (newState) { case PullDownToRefresh: /* * 下拉重新整理開始。正在下拉還沒鬆手時呼叫 * 每次重新下拉時,將圖片資源重置為小人的大腦袋*/ mImage.setImageResource(R.drawable.anim_pull_end); break; case Refreshing: /* * 正在重新整理。只調用一次 * 狀態切換為正在重新整理狀態時,設定圖片資源為小人賣萌的動畫並開始執行*/ mImage.setImageResource(R.drawable.anim_pull_refreshing); refreshingAnim = (AnimationDrawable) mImage.getDrawable(); refreshingAnim.start(); break; case ReleaseToRefresh: break; } } /** * 手指拖動下拉(會連續多次呼叫,新增isDragging並取代之前的onPulling、onReleasing) * @param isDragging true 手指正在拖動 false 回彈動畫 * @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+maxDragHeight) / footerHeight ) * @param offset 下拉的畫素偏移量 0 - offset - (footerHeight+maxDragHeight) * @param height 高度 HeaderHeight or FooterHeight * @param maxDragHeight 最大拖動高度 */ @Override public void onMoving(boolean isDragging, float percent, int offset, int height, int maxDragHeight) { //LogUtils.e("percent: " + percent); // 下拉的百分比小於100%時,不斷呼叫 setScale 方法改變圖片大小 if (percent < 1) { mImage.setScaleX(percent); mImage.setScaleY(percent); //是否執行過翻跟頭動畫的標記 if (hasSetPullDownAnim) { hasSetPullDownAnim = false; } } //當下拉的高度達到Header高度100%時,開始載入正在下拉的初始動畫,即翻跟頭 if (percent >= 1.0) { //因為這個方法是不停呼叫的,防止重複 if (!hasSetPullDownAnim) { mImage.setImageResource(R.drawable.anim_pull_end); pullDownAnim = (AnimationDrawable) mImage.getDrawable(); pullDownAnim.start(); hasSetPullDownAnim = true; } } } /** * 動畫結束 * @param refreshLayout RefreshLayout * @param success 資料是否成功重新整理或載入 * @return 完成動畫所需時間 如果返回 Integer.MAX_VALUE 將取消本次完成事件,繼續保持原有狀態 */ @Override public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) { // 結束動畫 if (pullDownAnim != null && pullDownAnim.isRunning()) { pullDownAnim.stop(); } if (refreshingAnim != null && refreshingAnim.isRunning()) { refreshingAnim.stop(); } //重置狀態 hasSetPullDownAnim = false; /*if (success ){ LogUtils.e("TRUE"); }else { LogUtils.e("FALSE"); }*/ return 0; } /** * 釋放時刻(呼叫一次,將會觸發載入) * @param refreshLayout RefreshLayout * @param height 高度 HeaderHeight or FooterHeight * @param maxDragHeight 最大拖動高度 */ @Override public void onReleased(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) { } @Override public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) { } /** * 是否支援水平方向的拖動(將會影響到onHorizontalDrag的呼叫) * @return 水平拖動需要消耗更多的時間和資源,所以如果不支援請返回false */ @Override public boolean isSupportHorizontalDrag() { return false; } }
邏輯主要在 onStateChanged()
和 onMoving()
方法裡,程式碼中註釋寫的很詳細。
切換狀態原理是每次都給 ImageView
設定對應的資源圖片或動畫檔案,然後得到 AnimationDrawable
開啟動畫,如下:
mImage.setImageResource(R.drawable.anim_pull_end);
pullDownAnim = (AnimationDrawable) mImage.getDrawable();
pullDownAnim.start();
程式碼中呼叫:
smartRefreshLayout.setRefreshHeader(new CustomRefreshHeader(getActivity())); smartRefreshLayout.setOnRefreshLoadmoreListener(new OnRefreshLoadmoreListener() { @Override public void onLoadmore(RefreshLayout refreshlayout) { Logger.d("onLoadmore"); smartRefreshLayout.finishLoadmore(2000, true); } @Override public void onRefresh(RefreshLayout refreshlayout) { Logger.d("onRefresh"); smartRefreshLayout.finishRefresh(2000, true); } });
貼出資源佈局檔案:widget_custom_refresh_header.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:gravity="center"
android:padding="5dp">
<ImageView
android:id="@+id/iv_refresh_header"
android:layout_width="41dp"
android:layout_height="54dp"
android:scaleX="0"
android:scaleY="0"
android:translationY="0dp" />
</LinearLayout>
anim_pull_end.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">
<item
android:drawable="@drawable/commonui_pull_end_image_frame_01"
android:duration="100" />
<item
android:drawable="@drawable/commonui_pull_end_image_frame_02"
android:duration="100" />
<item
android:drawable="@drawable/commonui_pull_end_image_frame_03"
android:duration="100" />
<item
android:drawable="@drawable/commonui_pull_end_image_frame_04"
android:duration="100" />
<item
android:drawable="@drawable/commonui_pull_end_image_frame_05"
android:duration="100" />
</animation-list>
anim_pull_refreshing.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list android:oneshot="false"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_01" />
<item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_02" />
<item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_03" />
<item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_02" />
<item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_05" />
<item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_06" />
<item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_07" />
<item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_06" />
</animation-list>