这篇的内容是介绍Piccolo的思路以及主要实现过程。
实现思路
要实现骨架屏占位加载,要解决以下几个问题:
- 实现占位效果
- 标记显示位置
- 对列表的支持
实现占位效果
为了便于自定义占位效果,以及更低的内存消耗,选择比View更轻量级的Drawable实现占位效果。
Piccolo提供了两种加载效果ImageShiningDrawable和TextShiningDrawable,其主要实现都是基于ShiningDrawable。类图关系如下:
ShiningDrawable实现了Shining接口,Shining接口对外提供动画控制方法。如果自定义有动效的Drawable,那么只需要实现Shining接口框架便会正确处理。
ShiningDrawable主要是让shader(着色器)平移变换,每次变换重新绘制来实现动效:
public ShiningDrawable(Shape s) {
super(s);
mMatrix = new Matrix();
ValueAnimator.AnimatorUpdateListener listener = animation -> {
mMatrix.setTranslate((Integer) animation.getAnimatedValue(), 0);
if (mShader != null) {
mShader.setLocalMatrix(mMatrix);
}
invalidateSelf();
};
mAnimator = ValueAnimator.ofInt().setDuration(1000);
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.setRepeatMode(ValueAnimator.RESTART);
mAnimator.addUpdateListener(listener);
setShape(new RectShape());
}
在bounds改变时设置平移范围:
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mAnimator.setIntValues(-bounds.width(), bounds.width());
if (mStarted && mPendingAnimator) {
mAnimator.start();
mPendingAnimator = false;
}
}
根据当前shader绘制显示内容:
@Override
protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
paint.setShader(mShader);
getShape().draw(canvas, paint);
}
TextShinigDrawable基于ShiningDrawable实现文本多行显示效果,ImageShiningDrawable则是基于TextShiningDrawable实现的单行显示效果。
标记显示位置
用一个继承FrameLayout的子类PiccoloLayout将需要显示占位效果的View包裹起来。PiccoloLayout有两个作用,一个是标记需要显示占位效果的view,另一个是作为占位Drawable的容器。类图如下:
占位Drawable需要在最顶层显示,所以使用FrameLayout的前景属性实现:
public void setMaskDrawable(Drawable drawable) {
mMaskDrawable = drawable;
if (mShowing) {
setForeground(mMaskDrawable);
}
}
PiccoloLayout同时提供对实现Shining接口的Drawable的控制:
public void setShining(boolean shining) {
if (mShining == shining) {
return;
}
mShining = shining;
if (mMaskDrawable instanceof Shining) {
Shining drawable = ((Shining) mMaskDrawable);
if (mShining && !drawable.isStarted()) {
drawable.start();
} else if (!mShining && drawable.isStarted()) {
drawable.cancel();
}
}
}
显示骨架屏:
public void show() {
if (mShowing) {
return;
}
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).setVisibility(View.INVISIBLE);
}
mShowing = true;
if (mMaskDrawable instanceof Shining) {
Shining drawable = ((Shining) mMaskDrawable);
if (mShining && !drawable.isStarted()) {
drawable.start();
}
}
setForeground(mMaskDrawable);
}
隐藏骨架屏:
public void hide() {
if (!mShowing) {
return;
}
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).setVisibility(View.VISIBLE);
}
mShowing = false;
if (mMaskDrawable instanceof Shining) {
Shining drawable = ((Shining) mMaskDrawable);
if (mShining && drawable.isStarted()) {
drawable.cancel();
}
}
setForeground(null);
}
在显示占位效果时,拦截事件:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mShowing) {
return true;
} else {
return super.onInterceptTouchEvent(ev);
}
}
对列表的支持
对RecyclerView这类通过Adapter生成View的情况,通过show和hide两种状态对Adapter进行替换的思路实现。框架提供ConductorForAdapter来方便应对这种情况。类图结构如下:
替换核心流程:
public void play(){
if(mVisible){
if (mView instanceof RecyclerView) {
((RecyclerView) mView).setAdapter(new PiccoloAdapter1(mItems));
} else if (mView instanceof ViewPager) {
((ViewPager) mView).setAdapter(new PiccoloAdapter3(mItems));
} else if (mView instanceof AbsListView) {
((AbsListView) mView).setAdapter(new PiccoloAdapter2(mView.getContext(), mItems));
} else {
throw new UnsupportedViewException();
}
}else{
if (mView instanceof RecyclerView) {
((RecyclerView) mView).setAdapter(mRecyclerAdapter);
} else if (mView instanceof ViewPager) {
((ViewPager) mView).setAdapter(mPagerAdapter);
} else if (mView instanceof AbsListView) {
((AbsListView) mView).setAdapter(mListAdapter);
} else {
throw new UnsupportedViewException();
}
}
}
为了统一使用api,对于单个View使用的情况也提供ConductorForView类。不再赘述。
github地址
Communication
等你来Android泡泡群冒泡哦!
QQ: 905487701