Android骨架屏效果的实现与原理解析

0、前言

大家在使用淘宝的时候,如下图所示有遇到这样的效果,其会只展示一部分骨架大致图,等数据加载完毕之后再展示真正的页面数据。与菊花图相比起来,这样的实现能更好的提升用户的体验,这种效果称做:Skeleton Screen Loading,中文叫做骨架屏

Android骨架屏效果的实现与原理解析_第1张图片

1、骨架屏的实现方式

在现在主流的骨架屏实现效果中有两种方式:

  • 通过View或者Adapter的替换来实现骨架屏效果。可以参考ShimmerRecyclerView、Skeleton及spruce-android。

  • 自定义一个View来对布局中的View进行一层包裹,当加载数据时则根据View来绘制骨架,否则显示正常UI,参考Skeleton Android。

这些开源库中,自己比较喜欢今天Skeleton这个开源库,总结了有如下一些优缺点:

优点:

  1. 代码方案实现及使用方式简单,通过替换View和Adapter实现效果,使用Builder设计模式来构造。
  2. 代码耦合程度不高。没有复杂的设计模式,使得代码结构清晰明了。
  3. 骨架屏的效果使用相对于较灵活,可以对整个布局实现骨架屏效果,也可以对单一View实现骨架屏效果。

缺点:

  1. 需要对每个骨架屏效果单独写一套xml布局。
  2. 使用的removeView和addView对 原有布局的view进行替换,存在一定的风险性
  3. 必须清晰的知道所bind的View类型,存在一定的类型转化问题。
  4. 依赖了shimmerlayout第三方库

2、Skeleton解读

一、Skeleton的使用方式

展示骨架屏效果:

View rootView = findViewById(R.id.rootView);
skeletonScreen = Skeleton.bind(rootView)
           .load(R.layout.activity_view_skeleton)//骨架屏UI
           .duration(1000)//动画时间,以毫秒为单位
           .shimmer(true)//是否开启动画
           .color(R.color.shimmer_color)//shimmer的颜色
           .angle(30)//shimmer的倾斜角度
           .show();

关闭骨架屏效果并展示原有View:

skeletonScreen.hide()

流程:

1. 选择需要替换的目标view
2. 将骨架效果xml与目标view进行绑定
3. 添加一些效果属性,比如:动画时间、是否开启展示动画、动画颜色等
4. 在合适的实际关闭骨架屏效果

二、Skeleton源码实现

Skeleton提供两个绑定方法,分别绑定普通View与RecyclerView,分别返回对应的Builder

public class Skeleton {
    public static RecyclerViewSkeletonScreen.Builder bind(RecyclerView recyclerView) {
        return new RecyclerViewSkeletonScreen.Builder(recyclerView);
    }
    public static ViewSkeletonScreen.Builder bind(View view) {
        return new ViewSkeletonScreen.Builder(view);
    }
}

我们首先来看看如何实现与普通View绑定,构造方法中传入目标View,并对shimmer动画效果设置默认的颜色,在Builder里面我们可以看到各种相关参数的设定。

public Builder(View view) {
	this.mView = view;
	this.mShimmerColor = ContextCompat.getColor(mView.getContext(), R.color.shimmer_color);
  }

接下来再到show的步骤,主要实现还是由ViewSkeletonScreen来实现

 public ViewSkeletonScreen show() {
     ViewSkeletonScreen skeletonScreen = new ViewSkeletonScreen(this);
     skeletonScreen.show();
     return skeletonScreen;
 }

其中ViewSkeletonScreen与绑定的RecyclerViewSkeletonScreen都实现了SkeletonScreen接口,SkeletonScreen有两个接口方法分别是

void show();
void hide();

对于ViewSkeletonScreen.show()进入源码,这里出现一个比较重要的类ViewReplacer,等下再进行解析,通过show的源码清楚的知道逻辑:
1、生成骨架效果View
2、利用生成的View替换目标View。

其中生成骨架效果View阶段主要还是通过LayoutInflater去加载传入mSkeletonResID

 @Override
 public void show() {
        View skeletonLoadingView = generateSkeletonLoadingView();
        if (skeletonLoadingView != null) {
            mViewReplacer.replace(skeletonLoadingView);
        }
    }

接下来主要讲解ViewReplacer类,其构造方法传入目标View

   public ViewReplacer(View sourceView) {
        mSourceView = sourceView;
        mSourceViewLayoutParams = mSourceView.getLayoutParams();
        mCurrentView = mSourceView;
        mSourceViewId = mSourceView.getId();
    }

其比较重要的方法有两个:replace()restore() 这两个方法分别为SkeletonScreen 的show()和hide()的最终实现,首先看replace()方法,有两个方法重载,分别传入targetViewResID或者targetView,最终还是会走到replace(View targetView)中。
其主要逻辑为:

1. 判断所替换的View和骨架屏效果View是否为同一个View
2. remove掉在父布局中的目标View
3. 将骨架屏效果View添加到目标View的父布局中

 public void replace(int targetViewResID) {
        if (mTargetViewResID == targetViewResID) {
            return;
        }
        if (init()) {
            mTargetViewResID = targetViewResID;
            replace(LayoutInflater.from(mSourceView.getContext()).inflate(mTargetViewResID, mSourceParentView, false));
        }
    }

    public void replace(View targetView) {
        if (mCurrentView == targetView) {
            return;
        }
        if (targetView.getParent() != null) {
            ((ViewGroup) targetView.getParent()).removeView(targetView);
        }
        if (init()) {
            mTargetView = targetView;
            mSourceParentView.removeView(mCurrentView);
            mTargetView.setId(mSourceViewId);
            mSourceParentView.addView(mTargetView, mSourceViewIndexInParent, mSourceViewLayoutParams);
            mCurrentView = mTargetView;
        }
    }

在执行添加到目标View的父布局中,有执行一个init方法,主要做两件事:

1. 获取目标View的父View
2. 找到目标View在父View 中的位置索引,为之后添加骨架屏View到父View中做铺垫

   private boolean init() {
        if (mSourceParentView == null) {
            mSourceParentView = (ViewGroup) mSourceView.getParent();
            if (mSourceParentView == null) {
                Log.e(TAG, "the source view have not attach to any view");
                return false;
            }
            int count = mSourceParentView.getChildCount();
            for (int index = 0; index < count; index++) {
                if (mSourceView == mSourceParentView.getChildAt(index)) {
                    mSourceViewIndexInParent = index;
                    break;
                }
            }
        }
        return true;
    }

至此对普通View的骨架屏效果实现流程已经完全梳理完成,那对于RecyclerView呢?其实两者实现逻辑差不多,主要有两个差异:

  1. RecyclerViewSkeletonScreen的Builder中,相比ViewSkeletonScreen多了一个adapter()方法,传入目标RecyclerViewAdapter
  2. 在show的时候对目标RecyclerView的adapter进行替换,使用骨架屏效果的adapter。hide的时候恢复为原先的Adapter

3、总结

  1. Skeleton的原理主要是通过替换目标View和RecyclerView的Adapter
  2. 在Skeleton的使用过程中最需要关心的两个问题是:show()和hide()的时机
  3. 对于整个页面的骨架屏效果实现,个人推荐在布局中添加一个全屏的空View盖在原先内容上
  4. 注意一些异常情况下的hide(),要不然整个页面就“假死”状态了。

参考:
https://juejin.im/post/5c789a4ce51d457c042d3b31

你可能感兴趣的:(Android)