在如今获取用户成本越来越高的情况下, 好的用户体验能够更好的留住用户. 为了提升产品的用户体验, 各种技术层出不穷, 其中, 尤以菊花图以及由它衍生出的各种加载动画最为突出.
对于菊花图, 想必是又爱又恨. 而如今有了比菊花图设计体验更棒的方法, 即常看到的 Skeleton Screen Loading, 中文叫做骨架屏.
那什么是骨架屏尼? 它的语义如下:
即表示在页面完全渲染完成之前, 用户会看到一个占位的样式, 用以描绘了当前页面的大致框架, 加载完成后, 最终骨架屏中各个占位部分将被真实的数据替换.
其效果图如下:
本着不重复造轮子的思想, 从 GitHub 上找了一些骨架屏的实现. 当然也可以自己来实现. 其最核心就是占位和属性动画的实现.
通过 View 或者 Adapter 的替换来实现骨架屏是最普遍的方案, 该方案需要单独为骨架屏页面进行布局, 如果页面过多或者比较复杂, 写起来就还是蛮繁琐的. 具体实现有 https://github.com/sharish/ShimmerRecyclerView , https://github.com/ethanhua/Skeleton 及 https://github.com/willowtreeapps/spruce-android 等开源库.
自定义一个 View 来对布局中的每个 View 进行一层包裹, 当加载数据时则根据 View 来绘制骨架, 否则显示正常 UI. 由于该方案需要将每个 View 包裹一层, 所以会增加额外的布局层次. 具体实现有 Skeleton Android https://github.com/rasoulmiri/Skeleton 等开源库.
上面就是目前在 Android 上实现骨架屏的两种方案, 下面以 Skeleton 及 Skeleton Android 为例进行讲解.
Skeleton
要想使用 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
要想使用 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, 也需要在构造方法里进行高度的测量. 所以这样就会限制比较大. 再来看布局文件的实现.
- <?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="@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
- ...>
- <!--Rect-->
- <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>
- <!--Oval-->
- <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>
- <!--Text-->
- <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 方便很多, 扩展性也更好一点. 当然我们也可以根据这两种方案的思想来自己实现骨架屏.
客户端骨架屏详解
来源: https://juejin.im/post/5c789a4ce51d457c042d3b31