本文已授權「玉剛說」微信公眾號獨家釋出
在如今獲取使用者成本越來越高的情況下,好的使用者體驗能夠更好的留住使用者。為了提升產品的使用者體驗,各種技術層出不窮,其中,尤以菊花圖以及由它衍生出的各種載入動畫最為突出。
對於菊花圖,想必是又愛又恨。而如今有了比菊花圖設計體驗更棒的方法,即常看到的Skeleton Screen Loading
,中文叫做骨架屏。
那什麼是骨架屏尼?它的語義如下:
即表示在頁面完全渲染完成之前,使用者會看到一個佔位的樣式,用以描繪了當前頁面的大致框架,載入完成後,最終骨架屏中各個佔位部分將被真實的資料替換。
其效果圖如下:
本著不重複造輪子的思想,從GitHub
上找了一些骨架屏的實現。當然也可以自己來實現。其最核心就是佔位和屬性動畫的實現。
- 通過
View
或者Adapter
的替換來實現骨架屏是最普遍的方案,該方案需要單獨為骨架屏頁面進行佈局,如果頁面過多或者比較複雜,寫起來就還是蠻繁瑣的。具體實現有ShimmerRecyclerView、Skeleton及spruce-android等開源庫。 - 自定義一個
View
來對佈局中的每個View
進行一層包裹,當載入資料時則根據View
來繪製骨架,否則顯示正常UI。由於該方案需要將每個View
包裹一層,所以會增加額外的佈局層次。具體實現有Skeleton Android等開源庫。
上面就是目前在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
方便很多,擴充套件性也更好一點。當然我們也可以根據這兩種方案的思想來自己實現骨架屏。