骨架屏 (Skeleton Screen) 在 Android

作者: Android开发架构 | 来源:发表于2019-03-05 16:31 被阅读8次

    在如今获取用户成本越来越高的情况下,好的用户体验能够更好的留住用户。为了提升产品的用户体验,各种技术层出不穷,其中,尤以菊花图以及由它衍生出的各种加载动画最为突出。

    对于菊花图,想必是又爱又恨。而如今有了比菊花图设计体验更棒的方法,即常看到的Skeleton Screen Loading,中文叫做骨架屏

    那什么是骨架屏尼?它的语义如下:

    即表示在页面完全渲染完成之前,用户会看到一个占位的样式,用以描绘了当前页面的大致框架,加载完成后,最终骨架屏中各个占位部分将被真实的数据替换。

    其效果图如下:

    本着不重复造轮子的思想,从GitHub上找了一些骨架屏的实现。当然也可以自己来实现。其最核心就是占位和属性动画的实现。

    • 通过View或者Adapter的替换来实现骨架屏是最普遍的方案,该方案需要单独为骨架屏页面进行布局,如果页面过多或者比较复杂,写起来就还是蛮繁琐的。具体实现有ShimmerRecyclerView、Skeleton及spruce-android等开源库。

    • 自定义一个View来对布局中的每个View进行一层包裹,当加载数据时则根据View来绘制骨架,否则显示正常UI。由于该方案需要将每个View包裹一层,所以会增加额外的布局层次。具体实现有Skeleton Android等开源库。

    上面就是目前在Android上实现骨架屏的两种方案,下面以SkeletonSkeleton 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()
    

    用法基本上不变,主要变化就在showhide这两个方法中。

    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-v7cardview-v7这两个库且版本可能较低,所以可能会存在版本冲突问题,解决方案如下。

    dependencies {
        implementation ('com.github.rasoulmiri:Skeleton:v1.0.9'){
            exclude group: 'com.android.support'
        }
    }
    

    先来看如何通过Skeleton AndroidRecyclerView上实现骨架屏。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 AndroidView上实现骨架屏。

      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>
    

    实现效果如下:

    image

    上面介绍了Skeleton Android的使用,它的原理基本上就是通过SkeletonGroupSkeletonView这两个控件来进行骨架的绘制。SkeletonGroupSkeletonView都是继承自RelativeLayout的自定义控件,SkeletonView起一个标识的作用,在SkeletonGroup中会将SkeletonView绘制成相应的长方形、圆形等骨架。

    总结

    前面介绍了骨架屏在Android上的应用。它们的区别主要是需不需要自己来实现骨架屏布局。但是从使用上来说Skeleton要比Skeleton Android方便很多,扩展性也更好一点。当然我们也可以根据这两种方案的思想来自己实现骨架屏。

    免费获取安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、Kotlin、混合式开发(ReactNative+Weex)和一线互联网公司关于android面试的题目汇总可以加:936332305 / 链接:点击链接加入【安卓开发架构】:https://jq.qq.com/?_wv=1027&k=515xp64

    相关文章

      网友评论

        本文标题:骨架屏 (Skeleton Screen) 在 Android

        本文链接:https://www.haomeiwen.com/subject/cojouqtx.html