本文已授权「玉刚说」微信公众号独家发布
在如今获取用户成本越来越高的情况下,好的用户体验能够更好的留住用户。为了提升产品的用户体验,各种技术层出不穷,其中,尤以菊花图以及由它衍生出的各种加载动画最为突出。
对于菊花图,想必是又爱又恨。而如今有了比菊花图设计体验更棒的方法,即常看到的Skeleton Screen Loading
,中文叫做骨架屏。
那什么是骨架屏尼?它的语义如下:
即表示在页面完全渲染完成之前,用户会看到一个占位的样式,用以描绘了当前页面的大致框架,加载完成后,最终骨架屏中各个占位部分将被真实的数据替换。
其效果图如下:
本着不重复造轮子的思想,从GitHub
上找了一些骨架屏的实现。当然也可以自己来实现。其最核心就是占位和属性动画的实现。
View
或者Adapter
的替换来实现骨架屏是最普遍的方案,该方案需要单独为骨架屏页面进行布局,如果页面过多或者比较复杂,写起来就还是蛮繁琐的。具体实现有ShimmerRecyclerView、Skeleton及spruce-android等开源库。View
来对布局中的每个View
进行一层包裹,当加载数据时则根据View
来绘制骨架,否则显示正常UI。由于该方案需要将每个View
包裹一层,所以会增加额外的布局层次。具体实现有Skeleton Android等开源库。 上面就是目前在Android上实现骨架屏的两种方案,下面以Skeleton
及Skeleton Android
为例进行讲解。
要想使用Skeleton
,需要先导入以下两个库。
dependencies {
implementation 'com.ethanhua:skeleton:1.1.2'
//主要是动画的实现
implementation 'io.supercharge:shimmerlayout:2.1.0'
}
skeleton
不仅支持在RecyclerView
上实现骨架屏,也支持在View
上实现骨架屏。
先来看看在RecyclerView
上的实现。
recyclerView = findViewById(R.id.recycler);
recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
//实际Adapter
NewsAdapter adapter = new NewsAdapter();
final SkeletonScreen skeletonScreen = Skeleton.bind(recyclerView)
.adapter(adapter)//设置实际adapter
.shimmer(true)//是否开启动画
.angle(30)//shimmer的倾斜角度
// .color(R.color.colorAccent)//shimmer的颜色
.frozen(true)//true则表示显示骨架屏时,RecyclerView不可滑动,否则可以滑动
.duration(1200)//动画时间,以毫秒为单位
.count(10)//显示骨架屏时item的个数
.load(R.layout.item_skeleton_news)//骨架屏UI
.show(); //default count is 10
recyclerView.postDelayed(new Runnable() {
@Override
public void run() {
skeletonScreen.hide();
}
}, 10000);//延迟时间
使用还是比较简单的,主要是对动画属性的设置。当调用show
方法时就会显示骨架屏,调用hide
就会隐藏骨架屏,显示正常UI。下面就来看看这两个方法的实现。
public class RecyclerViewSkeletonScreen implements SkeletonScreen {
//实际Adapter
private final RecyclerView.Adapter mActualAdapter;
//骨架UI所需Adapter
private final SkeletonAdapter mSkeletonAdapter;
...
@Override
public void show() {
//将骨架UI的Adapter设置给RecyclerView
mRecyclerView.setAdapter(mSkeletonAdapter);
if (!mRecyclerView.isComputingLayout() && mRecyclerViewFrozen) {
mRecyclerView.setLayoutFrozen(true);
}
}
@Override
public void hide() {
//将正常UI的Adapter设置给RecyclerView
mRecyclerView.setAdapter(mActualAdapter);
}
...
}
从上面可以看出,在RecycleView
上实现骨架屏是非常简单的,但需要为骨架屏单独实现一套布局,然后通过两个Adapter
替换即可。
虽然骨架屏很多时候都是用在列表、表格中使用,但也有在View
上使用的需求,下面就来看看如何在View
上实现骨架屏。
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();
MyHandler myHandler = new MyHandler(this);
myHandler.sendEmptyMessageDelayed(1, 10000);
//关闭骨架屏,显示正常UI
skeletonScreen.hide()
用法基本上不变,主要变化就在show
与hide
这两个方法中。
public class ViewSkeletonScreen implements SkeletonScreen {
//View替换的工具类
private final ViewReplacer mViewReplacer;
//实际View
private final View mActualView;
...
@Override
public void show() {
View skeletonLoadingView = generateSkeletonLoadingView();
if (skeletonLoadingView != null) {
//使用骨架屏UI替换实际UI
mViewReplacer.replace(skeletonLoadingView);
}
}
@Override
public void hide() {
if (mViewReplacer.getTargetView() instanceof ShimmerLayout) {
((ShimmerLayout) mViewReplacer.getTargetView()).stopShimmerAnimation();
}
//移除骨架屏UI,显示实际UI
mViewReplacer.restore();
}
...
}
//View替换实现类
public class ViewReplacer {
//实际UI所在的View
private final View mSourceView;
//骨架屏UI所在View
private View mTargetView;
...
public void replace(View targetView) {
...
if (init()) {
mTargetView = targetView;
//移除当前View,即实际UI所在View
mSourceParentView.removeView(mCurrentView);
mTargetView.setId(mSourceViewId);
//将骨架屏UI所在View添加进来
mSourceParentView.addView(mTargetView, mSourceViewIndexInParent, mSourceViewLayoutParams);
mCurrentView = mTargetView;
}
}
public void restore() {
if (mSourceParentView != null) {
//移除当前View,即骨架屏UI所在View
mSourceParentView.removeView(mCurrentView);
//将实际UI所在View添加进来
mSourceParentView.addView(mSourceView, mSourceViewIndexInParent, mSourceViewLayoutParams);
mCurrentView = mSourceView;
mTargetView = null;
mTargetViewResID = -1;
}
}
...
}
实现效果如下。
从上面可以看出,在View
上实现骨架屏也是非常简单的,也需要为骨架屏单独写一套布局,然后通过两个View
替换即可。
从使用及具体实现上可以发现Skeleton
还是蛮简单的。但最大的缺点就是要专门为骨架屏实现一套布局,比较繁琐。
要想使用Skeleton Android
,首先需要在项目根目录下的build.gradle
导入存储Skeleton Android
的仓库。
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
然后在app
目录下的build.gradle
文件中导入下面这个库即可。
dependencies {
compile 'com.github.rasoulmiri:Skeleton:v1.0.9'
}
这里有一点需要注意,引用该库会自动引用appcompat-v7
及cardview-v7
这两个库且版本可能较低,所以可能会存在版本冲突问题,解决方案如下。
dependencies {
implementation ('com.github.rasoulmiri:Skeleton:v1.0.9'){
exclude group: 'com.android.support'
}
}
先来看如何通过Skeleton Android
在RecyclerView
上实现骨架屏。Skeleton Android
相比Skeleton
最大的区别就是不需要专门为骨架屏实现一套布局,但使用起来就稍微复杂一些。
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
list = new ArrayList<>();
adapter = new PersonAdapter(this, list, recyclerView, new IsCanSetAdapterListener() {
@Override
public void isCanSet() {
recyclerView.setAdapter(adapter);
}
});
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
list.add("str" + i);
}
adapter.addMoreDataAndSkeletonFinish(list);
}
}, 5000);
//adapter的实现
public class PersonAdapter extends AdapterSkeleton<String, SimpleRcvViewHolder> {
public PersonAdapter(final Context context, final List<String> items, final RecyclerView recyclerView, final IsCanSetAdapterListener IsCanSetAdapterListener) {
this.context = context;
this.items = items;
this.isCanSetAdapterListener = IsCanSetAdapterListener;
measureHeightRecyclerViewAndItem(recyclerView, R.layout.item_person);// Set height
}
@Override
public SimpleRcvViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new SimpleRcvViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_person, parent, false));
}
@Override
public void onBindViewHolder(@NonNull SimpleRcvViewHolder holder, int position) {
SkeletonGroup skeletonGroup = holder.getView(R.id.skeleton_group);
if (skeletonConfig.isSkeletonIsOn()) {
//need show s for 2 cards
skeletonGroup.setAutoPlay(true);
return;
} else {
skeletonGroup.setShowSkeleton(false);
skeletonGroup.finishAnimation();
}
}
@Override
public int getItemCount() {
return 50;
}
}
在使用Skeleton Android
时需要我们自定义的Adapter
去继承AdapterSkeleton
,也需要在构造方法里进行高度的测量。所以这样就会限制比较大。再来看布局文件的实现。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_grid_item">
<io.rmiri.skeleton.SkeletonGroup
android:id="@+id/skeleton_group"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
...>
<io.rmiri.skeleton.SkeletonView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
... />
io.rmiri.skeleton.SkeletonView>
<io.rmiri.skeleton.SkeletonView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
... />
io.rmiri.skeleton.SkeletonView>
<io.rmiri.skeleton.SkeletonView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
... />
io.rmiri.skeleton.SkeletonView>
<io.rmiri.skeleton.SkeletonView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
... />
io.rmiri.skeleton.SkeletonView>
LinearLayout>
io.rmiri.skeleton.SkeletonGroup>
LinearLayout>
很明显增加了额外的布局层级。下面再来看通过Skeleton Android
在View
上实现骨架屏。
skeletonGroup = (SkeletonGroup) findViewById(R.id.skeletonGroup);
textTv = (TextView) findViewById(R.id.textTv);
skeletonGroup.setSkeletonListener(new SkeletonGroup.SkeletonListener() {
@Override
public void onStartAnimation() {
}
@Override
public void onFinishAnimation() {//显示加载数据
textTv.setText("The Android O release ultimately became Android 8.0 Oreo, as predicted by pretty much everyone the first time they thought of a sweet");
}
});
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
skeletonGroup.finishAnimation();
}
}, 5000);
比在RecycleView
上实现骨架屏简单多了,当然,布局文件里也需要将控件进行一层包裹。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:Skeleton="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<TextView
... />
<io.rmiri.skeleton.SkeletonGroup
android:id="@+id/skeletonGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
Skeleton:SK_BackgroundViewsColor="#EEEEEE"
Skeleton:SK_animationAutoStart="true"
Skeleton:SK_animationDirection="LTR"
Skeleton:SK_animationDuration="1000"
Skeleton:SK_animationFinishType="none"
Skeleton:SK_animationNormalType="alpha"
Skeleton:SK_backgroundMainColor="@android:color/transparent"
Skeleton:SK_highLightColor="#DEDEDE">
<LinearLayout
...>
<LinearLayout
...>
<TextView
.... />
<io.rmiri.skeleton.SkeletonView
android:layout_width="match_parent"
android:layout_height="wrap_content"
Skeleton:SK_shapeType="rect">
<TextView
... />
io.rmiri.skeleton.SkeletonView>
LinearLayout>
<LinearLayout
...>
<TextView
... />
<io.rmiri.skeleton.SkeletonView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Skeleton:SK_shapeType="oval">
<android.support.v7.widget.AppCompatImageButton
... />
io.rmiri.skeleton.SkeletonView>
LinearLayout>
<LinearLayout
...>
<TextView
... />
<io.rmiri.skeleton.SkeletonView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Skeleton:SK_shapeType="text"
Skeleton:SK_textLineHeight="16dp"
Skeleton:SK_textLineLastWidth="threeQuarters"
Skeleton:SK_textLineNumber="5"
Skeleton:SK_textLineSpaceVertical="4dp">
<TextView
... />
io.rmiri.skeleton.SkeletonView>
LinearLayout>
LinearLayout>
io.rmiri.skeleton.SkeletonGroup>
LinearLayout>
实现效果如下。
上面介绍了Skeleton Android
的使用,它的原理基本上就是通过SkeletonGroup
及SkeletonView
这两个控件来进行骨架的绘制。SkeletonGroup
及SkeletonView
都是继承自RelativeLayout
的自定义控件,SkeletonView
起一个标识的作用,在SkeletonGroup
中会将SkeletonView
绘制成相应的长方形、圆形等骨架。
前面介绍了骨架屏在Android上的应用。它们的区别主要是需不需要自己来实现骨架屏布局。但是从使用上来说Skeleton
要比Skeleton Android
方便很多,扩展性也更好一点。当然我们也可以根据这两种方案的思想来自己实现骨架屏。
客户端骨架屏详解