andoid Skeleton Screen框架源码分析(二)

当一个View需要骨架图时,来看下源码是怎么实现的。

typescript 复制代码
public class SkeletonStrategy {

    public static RecyclerViewSkeletonScreen.Builder bind(RecyclerView recyclerView) {
        return new RecyclerViewSkeletonScreen.Builder(recyclerView);
    }
    //来看这个方法,这是用于单个View
    public static ViewSkeletonScreen.Builder bind(View view) {
        return new ViewSkeletonScreen.Builder(view);
    }

}

和上篇的静态类Builder类似,

kotlin 复制代码
public static class Builder {
    //传入的View
    private final View mView;
    //占位资源id
    private int mSkeletonLayoutResID;
    //是否开启动画 默认为true
    private boolean mShimmer = true;
    //光晕的动画颜色
    private int mShimmerColor;
    //光晕动画时间  默认为1s
    private int mShimmerDuration = 1000;
    //光晕的动画角度 默认20
    private int mShimmerAngle = 20;

    public Builder(View view) {
        this.mView = view;
        this.mShimmerColor = ContextCompat.getColor(mView.getContext(), R.color.shimmer_color);
    }

    /**
     * @param skeletonLayoutResID the loading skeleton layoutResID
     */
    public Builder load(@LayoutRes int skeletonLayoutResID) {
        this.mSkeletonLayoutResID = skeletonLayoutResID;
        return this;
    }

    /**
     * @param shimmerColor the shimmer color
     */
    public Builder color(@ColorRes int shimmerColor) {
        this.mShimmerColor = ContextCompat.getColor(mView.getContext(), shimmerColor);
        return this;
    }

    /**
     * @param shimmer whether show shimmer animation
     */
    public ViewSkeletonScreen.Builder shimmer(boolean shimmer) {
        this.mShimmer = shimmer;
        return this;
    }

    /**
     * the duration of the animation , the time it will take for the highlight to move from one end of the layout
     * to the other.
     *
     * @param shimmerDuration Duration of the shimmer animation, in milliseconds
     */
    public ViewSkeletonScreen.Builder duration(int shimmerDuration) {
        this.mShimmerDuration = shimmerDuration;
        return this;
    }

    /**
     * @param shimmerAngle the angle of the shimmer effect in clockwise direction in degrees.
     */
    public ViewSkeletonScreen.Builder angle(@IntRange(from = 0, to = 30) int shimmerAngle) {
        this.mShimmerAngle = shimmerAngle;
        return this;
    }

    public ViewSkeletonScreen show() {
        ViewSkeletonScreen skeletonScreen = new ViewSkeletonScreen(this);
        skeletonScreen.show();
        return skeletonScreen;
    }

}

通过Builder的方法就是目的就是ViewSkeletonScreen的成员变量赋值的。

ini 复制代码
public class ViewSkeletonScreen implements SkeletonScreen {
    private static final String TAG = ViewSkeletonScreen.class.getName();
    private final ViewReplacer mViewReplacer;
    private final View mActualView;
    private final int mSkeletonResID;
    private final int mShimmerColor;
    private final boolean mShimmer;
    private final int mShimmerDuration;
    private final int mShimmerAngle;
    //这里就是通过Builder来给ViewSkeletonScreen的成员变量赋值。
    private ViewSkeletonScreen(Builder builder) {
        mActualView = builder.mView;
        mSkeletonResID = builder.mSkeletonLayoutResID;
        mShimmer = builder.mShimmer;
        mShimmerDuration = builder.mShimmerDuration;
        mShimmerAngle = builder.mShimmerAngle;
        mShimmerColor = builder.mShimmerColor;
        mViewReplacer = new ViewReplacer(builder.mView);
    }
    }

同样最后是调用show方法,

这里可以看到先生成一个LoadingView,然后再替换原来的View。

ini 复制代码
private View generateSkeletonLoadingView() {
    ViewParent viewParent = mActualView.getParent();
    if (viewParent == null) {
        Log.e(TAG, "the source view have not attach to any view");
        return null;
    }
    ViewGroup parentView = (ViewGroup) viewParent;
    if (mShimmer) {
        return generateShimmerContainerLayout(parentView);
    }
    return LayoutInflater.from(mActualView.getContext()).inflate(mSkeletonResID, parentView, false);
}

当mShimmer为false时,先是获取原来View(mActualView)的父View,然后结合mSkeletonResID生成一个View作为目标View去替换mActualView。 当mShimmer为true时,

scss 复制代码
private ShimmerLayout generateShimmerContainerLayout(ViewGroup parentView) {
    final ShimmerLayout shimmerLayout = (ShimmerLayout) LayoutInflater.from(mActualView.getContext()).inflate(R.layout.layout_shimmer, parentView, false);
    shimmerLayout.setShimmerColor(mShimmerColor);
    shimmerLayout.setShimmerAngle(mShimmerAngle);
    shimmerLayout.setShimmerAnimationDuration(mShimmerDuration);
    View innerView = LayoutInflater.from(mActualView.getContext()).inflate(mSkeletonResID, shimmerLayout, false);
    ViewGroup.LayoutParams lp = innerView.getLayoutParams();
    if (lp != null) {
        shimmerLayout.setLayoutParams(lp);
    }
    shimmerLayout.addView(innerView);
    shimmerLayout.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
        @Override
        public void onViewAttachedToWindow(View v) {
            shimmerLayout.startShimmerAnimation();
        }

        @Override
        public void onViewDetachedFromWindow(View v) {
            shimmerLayout.stopShimmerAnimation();
        }
    });
    shimmerLayout.startShimmerAnimation();
    return shimmerLayout;
}

先用parentView和R.layout.layout_shimmer生成一个ShimmerLayout

scala 复制代码
public class ShimmerLayout extends FrameLayout(下篇研究)

shimmerLayout接着设置它的一些属性,把用mSkeletonResID生成的View被添加了进去,然后就是shimmerLayoyt的监听,依附了Window时候就开启动画。

下面主要来看下ViewReplacer这个类

ini 复制代码
package com.ethanhua.skeleton;

import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by ethanhua on 2017/8/2.
 */

public class ViewReplacer {
    private static final String TAG = ViewReplacer.class.getName();
    private final View mSourceView;
    private View mTargetView;
    private int mTargetViewResID = -1;
    private View mCurrentView;
    private ViewGroup mSourceParentView;
    private final ViewGroup.LayoutParams mSourceViewLayoutParams;
    private int mSourceViewIndexInParent = 0;
    private final int mSourceViewId;

    public ViewReplacer(View sourceView) {
        mSourceView = sourceView;
        mSourceViewLayoutParams = mSourceView.getLayoutParams();
        mCurrentView = mSourceView;
        mSourceViewId = mSourceView.getId();
    }


    public void replace(int targetViewResID) {
        if (mTargetViewResID == targetViewResID) {
            return;
        }
        if (init()) {
            mTargetViewResID = targetViewResID;
            replace(LayoutInflater.from(mSourceView.getContext()).inflate(mTargetViewResID, mSourceParentView, false));
        }
    }

    public void replace(View targetView) {
        if (mCurrentView == targetView) {
            return;
        }
        if (targetView.getParent() != null) {
            ((ViewGroup) targetView.getParent()).removeView(targetView);
        }
        if (init()) {
            mTargetView = targetView;
            mSourceParentView.removeView(mCurrentView);
            mTargetView.setId(mSourceViewId);
            mSourceParentView.addView(mTargetView, mSourceViewIndexInParent, mSourceViewLayoutParams);
            mCurrentView = mTargetView;
        }
    }

    public void restore() {
        if (mSourceParentView != null) {
            mSourceParentView.removeView(mCurrentView);
            mSourceParentView.addView(mSourceView, mSourceViewIndexInParent, mSourceViewLayoutParams);
            mCurrentView = mSourceView;
            mTargetView = null;
            mTargetViewResID = -1;
        }
    }


    public View getSourceView() {
        return mSourceView;
    }

    public View getTargetView() {
        return mTargetView;
    }

    public View getCurrentView() {
        return mCurrentView;
    }

    private boolean init() {
        if (mSourceParentView == null) {
            mSourceParentView = (ViewGroup) mSourceView.getParent();
            if (mSourceParentView == null) {
                Log.e(TAG, "the source view have not attach to any view");
                return false;
            }
            int count = mSourceParentView.getChildCount();
            for (int index = 0; index < count; index++) {
                if (mSourceView == mSourceParentView.getChildAt(index)) {
                    mSourceViewIndexInParent = index;
                    break;
                }
            }
        }
        return true;
    }
}

先看

ini 复制代码
public void replace(View targetView) {
    if (mCurrentView == targetView) {
        return;
    }
    if (targetView.getParent() != null) {
        ((ViewGroup) targetView.getParent()).removeView(targetView);
    }
    if (init()) {
        mTargetView = targetView;
        mSourceParentView.removeView(mCurrentView);
        mTargetView.setId(mSourceViewId);
        mSourceParentView.addView(mTargetView, mSourceViewIndexInParent, mSourceViewLayoutParams);
        mCurrentView = mTargetView;
    }
}

如果当前View已经是targetView,那么直接return掉。 在替换成targetView之前,我们先判断一下先移除掉目标View,防止重复替换。 接下来就是

ini 复制代码
private boolean init() {
    if (mSourceParentView == null) {
        mSourceParentView = (ViewGroup) mSourceView.getParent();
        if (mSourceParentView == null) {
            Log.e(TAG, "the source view have not attach to any view");
            return false;
        }
        int count = mSourceParentView.getChildCount();
        for (int index = 0; index < count; index++) {
            if (mSourceView == mSourceParentView.getChildAt(index)) {
                mSourceViewIndexInParent = index;
                break;
            }
        }
    }
    return true;
}

这个方法主要就是给mSourceParentView赋值和找到mSourceViewIndexInParent这个值(下标)。 当init()方法返回true时,先移除掉mCurrentView,然后再把targetView添加进来。最后把mTargetView赋值给mCurrentView。

下面我们在看hide方法:

scss 复制代码
public void hide() {
    if (mViewReplacer.getTargetView() instanceof ShimmerLayout) {
        ((ShimmerLayout) mViewReplacer.getTargetView()).stopShimmerAnimation();
    }
    mViewReplacer.restore();
}

停止动画和恢复View。

ini 复制代码
public void restore() {
    if (mSourceParentView != null) {
        mSourceParentView.removeView(mCurrentView);
        mSourceParentView.addView(mSourceView, mSourceViewIndexInParent, mSourceViewLayoutParams);
        mCurrentView = mSourceView;
        mTargetView = null;
        mTargetViewResID = -1;
    }
}

先移除掉当前View,然后再把原来的View添加进来,再给mCurrentView赋值为mSourceView,接着把mTargetView置位null,id资源也赋值为-1。

相关推荐
彭于晏6892 小时前
Android高级控件
android·java·android-studio
666xiaoniuzi7 小时前
深入理解 C 语言中的内存操作函数:memcpy、memmove、memset 和 memcmp
android·c语言·数据库
沐言人生12 小时前
Android10 Framework—Init进程-8.服务端属性文件创建和mmap映射
android
沐言人生12 小时前
Android10 Framework—Init进程-9.服务端属性值初始化
android·android studio·android jetpack
沐言人生12 小时前
Android10 Framework—Init进程-7.服务端属性安全上下文序列化
android·android studio·android jetpack
追光天使12 小时前
【Mac】和【安卓手机】 通过有线方式实现投屏
android·macos·智能手机·投屏·有线
小雨cc5566ru13 小时前
uniapp+Android智慧居家养老服务平台 0fjae微信小程序
android·微信小程序·uni-app
一切皆是定数14 小时前
Android车载——VehicleHal初始化(Android 11)
android·gitee
一切皆是定数14 小时前
Android车载——VehicleHal运行流程(Android 11)
android