大家在使用淘宝的时候,如下图所示有遇到这样的效果,其会只展示一部分骨架大致图,等数据加载完毕之后再展示真正的页面数据。与菊花图相比起来,这样的实现能更好的提升用户的体验,这种效果称做:Skeleton Screen Loading,中文叫做骨架屏。
在现在主流的骨架屏实现效果中有两种方式:
通过View或者Adapter的替换来实现骨架屏效果。可以参考ShimmerRecyclerView、Skeleton及spruce-android。
自定义一个View来对布局中的View进行一层包裹,当加载数据时则根据View来绘制骨架,否则显示正常UI,参考Skeleton Android。
这些开源库中,自己比较喜欢今天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
呢?其实两者实现逻辑差不多,主要有两个差异:
RecyclerViewSkeletonScreen
的Builder中,相比ViewSkeletonScreen多了一个adapter()方法,传入目标RecyclerView
的Adapter
RecyclerView
的adapter进行替换,使用骨架屏效果的adapter。hide的时候恢复为原先的Adapter参考:
https://juejin.im/post/5c789a4ce51d457c042d3b31