Android实现骨架屏加载效果(二)

663 阅读2分钟

这篇的内容是介绍Piccolo的思路以及主要实现过程。

实现思路

要实现骨架屏占位加载,要解决以下几个问题:

  1. 实现占位效果
  2. 标记显示位置
  3. 对列表的支持

实现占位效果

为了便于自定义占位效果,以及更低的内存消耗,选择比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类。不再赘述。

Android实现骨架屏加载效果(一)

github地址

piccolo

Communication

等你来Android泡泡群冒泡哦!

QQ: 905487701