这是 [从零撸美团] 系列文章第二篇. 项目地址: https://github.com/cachecats/LikeMeiTuan
今天写了下拉刷新, 框架用的是 https://github.com/scwang90/SmartRefreshLayout , 不为啥, 因为 GitHub 上它有 9.5k 个 star, 中文支持好节省时间.
先上图:
一, 分析
美团的下拉加载动画初看挺简单的, 就一个卖萌的小人. 细看的话还稍微有点复杂, 一共有三个状态.
刚开始下拉的时候, 小脑袋从小变大的过程.
下拉到一定程度但还没松手, 小人翻了个跟头直到完全出现. 再往下拉保持最后完全出现的状态.
松开后左右摇头卖萌直至加载结束回弹回去.
这是三个动画啊! 真佩服这些大厂, 简单的加载动画都搞这么复杂..
分析完过程该想怎么实现了.
二, 反编译 App 看实现原理
最简单直白的方法就是反编译美团 App, 虽然看不到代码但资源文件能还原出来, 图片和 xml 文件完美还原.
反编译工具是 apktool https://ibotpeaches.github.io/Apktool/ , 使用方法官网上都有就不啰嗦了.
大部分图片都放在 res/drawable-xhdpi-v4 和 res/drawable-xxhdpi-v4 两个文件夹内, 仔细找下能看到多张连续的 loading 图片. 这里给美团程序猿点个赞, 文件命名都很规范, 很好找~
看到图片后知道原来它用的是最普通的帧动画啊, 也不是太复杂. 拿到资源图片, 知道实现原理, 就开工吧!
三, 实现动画效果
首先自定义 View CustomRefreshHeader 继承自 LinearLayout, 并实现 SmartRefreshLayout 的 RefreshHeader 接口. 然后主要就是重写 RefreshHeader 接口中的方法, 里面提供了下拉刷新时不同阶段的回调, 找到对应的方法码代码就好.
- public class CustomRefreshHeader extends LinearLayout implements RefreshHeader {
- private ImageView mImage;
- private AnimationDrawable pullDownAnim;
- private AnimationDrawable refreshingAnim;
- private boolean hasSetPullDownAnim = false;
- public CustomRefreshHeader(Context context) {
- this(context, null, 0);
- }
- public CustomRefreshHeader(Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
- public CustomRefreshHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- View view = View.inflate(context, R.layout.widget_custom_refresh_header, this);
- mImage = (ImageView) view.findViewById(R.id.iv_refresh_header);
- }
- @NonNull
- @Override
- public View getView() {
- return this;
- }
- @Override
- public SpinnerStyle getSpinnerStyle() {
- return SpinnerStyle.Translate;
- }
- @Override
- public void onStartAnimator(RefreshLayout layout, int height, int extendHeight) {
- }
- /**
- * 状态改变时调用. 在这里切换第三阶段的动画卖萌小人
- * @param refreshLayout
- * @param oldState
- * @param newState
- */
- @Override
- public void onStateChanged(RefreshLayout refreshLayout, RefreshState oldState, RefreshState newState) {
- switch (newState) {
- case PullDownToRefresh: // 下拉刷新开始. 正在下拉还没松手时调用
- // 每次重新下拉时, 将图片资源重置为小人的大脑袋
- mImage.setImageResource(R.drawable.commonui_pull_image);
- break;
- case Refreshing: // 正在刷新. 只调用一次
- // 状态切换为正在刷新状态时, 设置图片资源为小人卖萌的动画并开始执行
- mImage.setImageResource(R.drawable.anim_pull_refreshing);
- refreshingAnim = (AnimationDrawable) mImage.getDrawable();
- refreshingAnim.start();
- break;
- case ReleaseToRefresh:
- break;
- }
- }
- /**
- * 下拉过程中不断调用此方法. 第一阶段从小变大的小人头动画, 和第二阶段翻跟头动画都在这里设置
- */
- @Override
- public void onPullingDown(float percent, int offset, int headerHeight, int extendHeight) {
- Logger.d("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;
- }
- }
- }
- /**
- * 动画结束后调用
- */
- @Override
- public int onFinish(RefreshLayout layout, boolean success) {
- // 结束动画
- if (pullDownAnim != null && pullDownAnim.isRunning()) {
- pullDownAnim.stop();
- }
- if (refreshingAnim != null && refreshingAnim.isRunning()) {
- refreshingAnim.stop();
- }
- // 重置状态
- hasSetPullDownAnim = false;
- return 0;
- }
- @Override
- public void onReleasing(float percent, int offset, int headerHeight, int extendHeight) {
- }
- @Override
- public void onRefreshReleased(RefreshLayout layout, int headerHeight, int extendHeight) {
- }
- @Override
- public void setPrimaryColors(int... colors) {
- }
- @Override
- public void onInitialized(RefreshKernel kernel, int height, int extendHeight) {
- }
- @Override
- public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {
- }
- @Override
- public boolean isSupportHorizontalDrag() {
- return false;
- }
- }
逻辑主要在 onStateChanged() 和 onPullingDown() 方法里, 代码中注释写的很详细. 切换状态原理是每次都给 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>
好啦, 以上就是仿美团下拉刷新自定义动画的实现过程. 源码地址: https://github.com/cachecats/LikeMeiTuan
来源: https://juejin.im/post/5bdd272a6fb9a049de6ccc69