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。

相关推荐
sun0077005 小时前
android ndk编译valgrind
android
AI视觉网奇6 小时前
android studio 断点无效
android·ide·android studio
jiaxi的天空6 小时前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet7 小时前
android组包时会把从maven私服获取的包下载到本地吗
android
catchadmin7 小时前
PHP serialize 序列化完全指南
android·开发语言·php
tangweiguo030519879 小时前
Kable使用指南:Android BLE开发的现代化解决方案
android·kotlin
00后程序员张11 小时前
iOS App 混淆与资源保护:iOS配置文件加密、ipa文件安全、代码与多媒体资源防护全流程指南
android·安全·ios·小程序·uni-app·cocoa·iphone
柳岸风12 小时前
Android Studio Meerkat | 2024.3.1 Gradle Tasks不展示
android·ide·android studio
编程乐学12 小时前
安卓原创--基于 Android 开发的菜单管理系统
android
whatever who cares15 小时前
android中ViewModel 和 onSaveInstanceState 的最佳使用方法
android