当一个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。