Android 字符串测绘参考值

private void drawText(Canvas canvas, int x, int y, int width, int height){
        Paint.FontMetrics fontMetrics = paintText.getFontMetrics();

        canvas.drawText("TESThf我y", x, y, paintText);

        int line1 = y;
        int line2 = (int) (line1 + fontMetrics.top);
        int line3 = (int) (line1 + fontMetrics.ascent);
        int line4 = (int) (line1 + fontMetrics.descent);
        int line5 = (int) (line1 + fontMetrics.leading);
        int line6 = (int) (line1 + fontMetrics.bottom);

        // line2 top = y + top
        canvas.drawLine(0, line2, width, line2, linePaint);
        canvas.drawLine(20, line2, 20 + 100, line2 - 100, linePaint);
        canvas.drawText("top:" + fontMetrics.top, 100 + 20, line2 - 100, linePaint);

        // line3 ascent = y + ascent 上升
        canvas.drawLine(0, line3, width, line3, linePaint);
        canvas.drawLine(40, line3, 40 + 100, line3 - 100, linePaint);
        canvas.drawText("ascent:" + fontMetrics.ascent, 100 + 40, line3 - 100, linePaint);

        // line1 base = y
        canvas.drawLine(0, line1, width, line1, linePaint);
        canvas.drawLine(60, line1, 60 + 100, line1 + 100, linePaint);
        canvas.drawText("Base", 60 + 100, line1 + 100, linePaint);

        // line4 descent = y + descent 下降
        canvas.drawLine(0, line4, width, line4, linePaint);
        canvas.drawLine(0, line4, 100, line4 + 100, linePaint);
        canvas.drawText("descent:" + fontMetrics.descent, 100, line4 + 100, linePaint);

        // line5 leading = y + leading 行距
        canvas.drawLine(0, line5, width, line5, linePaint);
        canvas.drawLine(200, line5, 200 + 100, line5 + 100, linePaint);
        canvas.drawText("leading:" + fontMetrics.leading, 200 + 100, line5 + 100, linePaint);

        // line6 bottom = y + bottom
        canvas.drawLine(0, line6, width, line6, linePaint);
        canvas.drawLine(400, line6, 400 + 100, line6 + 100, linePaint);
        canvas.drawText("bottom:" + fontMetrics.bottom, 400 + 100, line6 + 100, linePaint);
    }

 

LinearLayout和RelativeLayout绘制过程的对比

在平时设计UI时,相信大多数人用得比较多的布局就是LinearLayout和RelativeLayout了,毕竟这两种布局能实现我们一般的需求。还记得刚开始学Android时,使用Eclipse进行开发,每次新建一个布局文件,该布局文件默认使用的布局是LinearLayout。后来改用Android Studio开发,SDK版本更新之后,每次新建的布局文件改成用RelativeLayout作为默认布局了。 很多人说这个Google团队出于性能考虑而进行的优化,的确,RelativeLayout能减少布局嵌套从而提高布局的性能,而使用Linearlayout如果不注意的话可能出现布局嵌套层次太深,从而大大降低了布局的性能。但是LinearLayout真的比不上RelativeLayout吗?答案是否定的。

View的绘制一般都是经过onMeasure,onLayout,和onDraw,LinearLayout和RelativeLayout都是继承自ViewGroup,而ViewGroup又是继承View的,所以LinearLayout和RelativeLayout一般也会经历上面所说的三个流程。但是在ViewGroup一般默认不调用onDraw,因为ViewGroup是容器,一般只用来装载控件,所以负责测量控件总共所需的宽高,并将控件放在我们指定的位置就行了。所以接下来我打算只从onMeasure和onLayout两个方面对比LinearLayout和RelativeLayout。

先看看LinearLayout的onMeasure方法:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

我们都知道LinearLayout有两个方向走向,一个是竖直方向,一个是水平方向。可以看出Linearlayout在测量宽高时是根据我们设置的方向分别调用不同的测量方法,这两个测量方法大致一样,我们以measureVertical来说明LinearLayout的具体测量过程吧。因为measureVertical这个方法有点长,所以我们贴部分代码说明问题就行了。

// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
      heightMeasureSpec, usedHeight);

在measureVertical方法里面调用了这个方法,什么意思呢?我们都知道在LinearLayout中,我们可以使用layout_weight属性,当子View使用了layout_weight属性时就会调用这个方法,这个方法的大概步骤是先不限制设置了weight属性的子View的大小,只要容器剩余的空间还足够就不对该子View作处理,但凡事都有个度,所以不可能每个设置了weight属性的子View都无限大吧,所以当所有子View测量完毕后,会对设置了weight属性的子View再调用一次measure方法,如下:

// We have no limit, so make all weighted views as tall as the largest child.
// Children will have already been measured once.
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null || child.getVisibility() == View.GONE) {
            continue;
        }

        final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

        float childExtra = lp.weight;
        if (childExtra > 0) {
            child.measure(MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY),
                                MeasureSpec.makeMeasureSpec(largestChildHeight, MeasureSpec.EXACTLY));
       }
   }
}

接下来说说RelativeLayout的onMeasure方法,部分源码如下:

        for (int i = 0; i < count; i++) {
            View child = views[i];
            if (child.getVisibility() != GONE) {
                LayoutParams params = (LayoutParams) child.getLayoutParams();
                int[] rules = params.getRules(layoutDirection);

                applyHorizontalSizeRules(params, myWidth, rules);
                measureChildHorizontal(child, params, myWidth, myHeight);

                if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
                    offsetHorizontalAxis = true;
                }
            }
        }
        for (int i = 0; i < count; i++) {
            final View child = views[i];
            if (child.getVisibility() != GONE) {
                final LayoutParams params = (LayoutParams) child.getLayoutParams();

                applyVerticalSizeRules(params, myHeight, child.getBaseline());
                measureChild(child, params, myWidth, myHeight);
                if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
                    offsetVerticalAxis = true;
                }

                if (isWrapContentWidth) {
                    if (isLayoutRtl()) {
                        if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                            width = Math.max(width, myWidth - params.mLeft);
                        } else {
                            width = Math.max(width, myWidth - params.mLeft - params.leftMargin);
                        }
                    } else {
                        if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                            width = Math.max(width, params.mRight);
                        } else {
                            width = Math.max(width, params.mRight + params.rightMargin);
                        }
                    }
                }

                if (isWrapContentHeight) {
                    if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                        height = Math.max(height, params.mBottom);
                    } else {
                        height = Math.max(height, params.mBottom + params.bottomMargin);
                    }
                }

                if (child != ignore || verticalGravity) {
                    left = Math.min(left, params.mLeft - params.leftMargin);
                    top = Math.min(top, params.mTop - params.topMargin);
                }

                if (child != ignore || horizontalGravity) {
                    right = Math.max(right, params.mRight + params.rightMargin);
                    bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
                }
            }
        }

由上面源码我们可知RelativeLayout对子View分别进行了竖直和水平方向的两次测量,这不难理解,因为RelativeLayout里面每个子View的位置都是相对的,所以要分别从水平和竖直两个方向对子View进行测量。

接下来在来看看LinearLayout和RelativeLayout的onLayout方法。同样的,先看LinearLayout的onLayout方法,如下:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

简单分析这个方法,即先根据LinearLayout设置的方向调用不同的方法,和上面的onMeasure方法中调用的情况类似,在这两个方法做的事情也差不多,就是将子View逐个按照其大小和布局参数摆放在对应的位置。

再看看RelativeLayout中onLayout方法,如下:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //  The layout has actually already been performed and the positions
        //  cached.  Apply the cached values to the children.
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                RelativeLayout.LayoutParams st =
                        (RelativeLayout.LayoutParams) child.getLayoutParams();
                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
            }
        }
    }

RelativeLayout的onLayout方法和LinearLayout的onLayout方法类似,都是遍历所有的子View,逐个逐个地将子View放在对应的位置,所以两者的onLayout方法性能相差不大。

总结:LinearLayout和RelativeLayout的性能差别主要体在onMeasure方法上,RelativeLayout始终要从竖直和水平两个方向对子View进行测量,而Linearlayout,当我们没有在子View中使用layout_weight属性时,LinearLayout只需对子View进行一次测量,反则需要对子View进行两次测量以确定最终大小,所以如果可以我们尽量少用layout_weight属性。在使用这两个布局之前,我们可以先进行衡量,如果需要实现的布局嵌套层次不深或者嵌套层次已经固定了,可以考虑用LinearLayout,相对的,如果某个布局嵌套层次很深,此时应该考虑使用RelativeLayout来减少嵌套层析从而优化布局的性能。

LayoutInflater中inflate方法的三个参数

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)  

1.1 root不为null,attachToRoot为true

表示将resource指定的布局添加到root中,添加的过程中resource所指定的的布局的根节点的各个属性都是有效的

Activity布局:

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:orientation="vertical"  
    android:id="@+id/ll"  
    tools:context="org.sang.layoutinflater.MainActivity">  
</LinearLayout>

linearlayout.xml布局:

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:id="@+id/ll"  
    android:layout_width="200dp"  
    android:layout_height="200dp"  
    android:background="@color/colorPrimary"  
    android:gravity="center"  
    android:orientation="vertical">  

    <Button  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content" />  
</LinearLayout>

java代码:

@Override  
protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    setContentView(R.layout.activity_main);  
    LinearLayout ll = (LinearLayout) findViewById(R.id.ll);  
    LayoutInflater inflater = LayoutInflater.from(this);  
    inflater.inflate(R.layout.linearlayout, ll,true);  
}  

这里我们都没写将inflate出来的View添加到ll中的代码,但是linearlayout布局文件就已经添加进来了。

这就是因为我第三个参数设置为了true,表示将第一个参数所指定的布局添加到第二个参数的View中。

1.2 root不为null,attachToRoot为false

如果root不为null,而attachToRoot为false的话,表示不将第一个参数所指定的View添加到root中。

那么这个时候有的小伙伴可能就有疑问了,既然不添加到root中,那我还写这么多干嘛?第二个参数直接给null不就可以了?

其实不然,这里涉及到另外一个问题:我们在开发的过程中给控件所指定的layout_width和layout_height到底是什么意思?该属性的表示一个控件在容器中的大小,就是说这个控件必须在容器中,这个属性才有意义,否则无意义。

这就意味着如果我直接将linearlayout加载进来而不给它指定一个父布局,则inflate布局的根节点的layout_width和layout_height属性将会失效(因为这个时候linearlayout将不处于任何容器中,那么它的根节点的宽高自然会失效)。

如果我想让linearlayout的根节点有效,又不想让其处于某一个容器中,那我就可以设置root不为null,而attachToRoot为false。

这样,指定root的目的也就很明确了,即root会协助linearlayout的根节点生成布局参数,只有这一个作用。

OK,还是上面的布局文件,如果我想将之添加到activity的布局中又该如何呢?

protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    setContentView(R.layout.activity_main);  
    LinearLayout ll = (LinearLayout) findViewById(R.id.ll);  
    LayoutInflater inflater = LayoutInflater.from(this);  
    View view = inflater.inflate(R.layout.linearlayout, ll, false);  
    ll.addView(view);  
}  

Android中View的Draw

View中:

  1. Draw the background 绘制背景
  2. If necessary, save the canvas’ layers to prepare for fading. 如有必要,颜色渐变淡之前保存画布层(即锁定原有的画布内容)
  3. Draw view’s content. 绘制view的内容
  4. Draw children. 绘制子view
  5. If necessary,draw the fading edges and restore layers. 如有必要,绘制颜色渐变淡的边框,并恢复画布(即画布改变的内容附加到原有内容上)
  6. Draw decorations (scrollbars for instance). 绘制装饰,比如滚动条
public void draw(Canvas canvas){
    ...
    if(!dirtyOpaque){
        drawBackground(canvas);//背景绘制
    }
    // skip step 2 & 5 if possible (common case) 通常情况跳过第2和第5步
    ...
    if(!dirtyQpaque) onDraw(canvas); // 调用onDraw
    dispatchDraw(canvas); // 绘制子view
    onDrawScrollBars(canvas); // 绘制滚动条
    ...
}

protected void dispatchDraw(Canvas canvas){// 空实现}

protected void onDraw(Canvas canvas){// 空实现}

ViewGroup中:

protected void dispatchDraw(Canvas canvas){
    ...
    drawChild(...); // 绘制子View
    ...
}

protected boolean drawChild(Canvas canvas, View child, long drawingTime){
    return child.draw(canvas, this, drawingTime);
}

说明:

  1. 自定义一个View时,重写onDraw。
    调用view.invalidate(), 会触发onDraw和computeScroll(). 前提是该View被附加在当前窗口
    view.postInvalidate();//是在非UI线程上调用的

  2. 自定义一个ViewGroup,重写onDraw。
    onDraw可能不会被调用,原因是需要先设置一个背景(颜色或图)
    表示这个group有东西需要绘制了,才会触发draw,之后是onDraw。
    因此,一般直接重写dispatchDraw来绘制viewGroup

  3. 自定义一个ViewGroup
    dispatchDraw会调用drawChild。

View.setWillNotDraw()方法的使用

这个函数更多被在ViewGroup上调用。

View

自定义View中如果重写了onDraw()即自定义了绘制,那么就应该在构造函数中调用view的setWillNotDraw(false),设置该flag标志。其实默认该标志就是false。

ViewGroup

ViewGroup默认情况下,出于性能考虑,会被设置成WILL_NOT_DROW,这样,ondraw就不会被执行了。

如果我们想重写一个viewgroup的ondraw方法,有两种方法:

  1. 构造函数中,给viewgroup设置一个颜色。
  2. 构造函数中,调用setWillNotDraw(false),去掉其WILL_NOT_DRAW flag。

在viewgroup初始化的时候,它调用了一个私有方法:initViewGroup,它里面会有一句setFlags(WILLL_NOT_DRAW,DRAW_MASK);相当于调用了setWillNotDraw(true)
所以说,对于ViewGroup,他就认为是透明的了,如果我们想要重写onDraw,就要调用setWillNotDraw(false)。

试分析-ShimmerFrameLayout

数据结构

public class ShimmerFrameLayout extends FrameLayout{
    // enum specity(特殊) the shape(形状) of the highlight mask applied to the contained view
    // 内容控件高光蒙版效果的枚举
    public enum MaskShape{
        LINEAR, // 线性效果
        RADIAL  // 辐射效果
    }
    // enum controlling the angle of the highlight mask animation
    // 控制高光蒙版动画的角度
    public enum MaskAngel{
        CW_0, // left to right      水平向右划过
        CW_90, // top to bottom     垂直向下落
        CW_180, // right to left    水平向左划过
        CW_270, // bottom to top    垂直向上冲
    }
    // struct storing(存储) various(各种) mask related(相关) parameters, which are used to construct the mask bitmap
    // 存放各种蒙版参数的类结构,这些参数是用于创建蒙版位图
    private static class Mask{
        public MaskAngle angle; // 动画角度
        public float tilt;      // 动画角度偏移
        public float dropoff;   // 掉落??
        public int fixedWidth;  // 固定宽度
        public int fixedHeight; // 固定高度
        public float intensity; // 强度
        public float relativeWidth; // 相对宽度
        public float relativeHeight; // 相对高度
        public MaskShape shape; // 动画形状
    }
    // struct for storing the mask translation animation values
    // 存放蒙版移动动画的起始点信息
    private staic class MaskTranslation{
        public int fromX;
        public int fromY;
        public int toX;
        public int toY;
    }
}

流程步骤

  1. 在构造方法中初始化参数
  2. 启动动画(自动启动或主动调用)
  3. 在动画中刷新参数,并调用invalidate(),触发逻辑
  4. 在dispatchDraw中,使用一个Bitmap结合子控件内容绘制

构造方法

    setWillNotDraw(false);
    mAlphaPaint = new Paint();
    mMaskPaint = new Paint();
    // 初始化
    useDefaults();

    if (attrs != null) {
        // 读取属性
    }

setWillNotDraw

动画

/**
   * Start the shimmer animation. If the 'auto start' property is set, this method is called automatically when the
   * layout is attached to the current window. Calling this method has no effect if the animation is already playing.
   * 如果设置为自动开启动画(auto start),这个方法当绑定到窗口之后就会自动调用
   */
  public void startShimmerAnimation() {
    if (mAnimationStarted) {
      return;
    }
    Animator animator = getShimmerAnimation();
    animator.start();
    mAnimationStarted = true;
  }

  /**
   * Stop the shimmer animation. Calling this method has no effect if the animation hasn't been started yet.
   */
  public void stopShimmerAnimation() {
    if (mAnimator != null) {
      mAnimator.end();
      mAnimator.removeAllUpdateListeners();
      mAnimator.cancel();
    }
    mAnimator = null;
    mAnimationStarted = false;
  }

  @Override
  protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    if (mGlobalLayoutListener == null) {
      mGlobalLayoutListener = getLayoutListener();
    }
    // 绑定到窗口后,等子视图都摆放好后,就会尝试启动动画
    getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
  }

  private ViewTreeObserver.OnGlobalLayoutListener getLayoutListener() {
    return new ViewTreeObserver.OnGlobalLayoutListener() {
      @Override
      public void onGlobalLayout() {
        boolean animationStarted = mAnimationStarted;
        resetAll();
        if (mAutoStart || animationStarted) {
          startShimmerAnimation();
        }
      }
    };
  }

  @Override
  protected void onDetachedFromWindow() {
    stopShimmerAnimation();
    if (mGlobalLayoutListener != null) {
      getViewTreeObserver().removeGlobalOnLayoutListener(mGlobalLayoutListener);
      mGlobalLayoutListener = null;
    }
    super.onDetachedFromWindow();
  }

动画和绘制

动画改变数据

  // Get the shimmer Animator object, which is responsible(负责的) for driving the highlight mask animation.
  private Animator getShimmerAnimation() {
    if (mAnimator != null) {
      return mAnimator;
    }
    int width = getWidth();
    int height = getHeight();
    switch (mMask.shape) {
      default:
      case LINEAR:
        switch (mMask.angle) {
          default: case CW_0: mMaskTranslation.set(-width, 0, width, 0); break;
          case CW_90: mMaskTranslation.set(0, -height, 0, height); break;
          case CW_180: mMaskTranslation.set(width, 0, -width, 0); break;
          case CW_270: mMaskTranslation.set(0, height, 0, -height); break;
        }
    }
    mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f + (float) mRepeatDelay / mDuration);
    mAnimator.setDuration(mDuration + mRepeatDelay);
    mAnimator.setRepeatCount(mRepeatCount);
    mAnimator.setRepeatMode(mRepeatMode);
    mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        float value = Math.max(0.0f, Math.min(1.0f, (Float) animation.getAnimatedValue()));
        setMaskOffsetX((int) (mMaskTranslation.fromX * (1 - value) + mMaskTranslation.toX * value));
        setMaskOffsetY((int) (mMaskTranslation.fromY * (1 - value) + mMaskTranslation.toY * value));
      }
    });
    return mAnimator;
  }

  /**
   * Translate the mask offset horizontally. Used by the animator.
   *
   * @param maskOffsetX Horizontal translation offset of the mask
   */
  private void setMaskOffsetX(int maskOffsetX) {
    if (mMaskOffsetX == maskOffsetX) {
      return;
    }
    mMaskOffsetX = maskOffsetX;
    invalidate(); // 使其重绘
  }

  /**
   * Translate the mask offset vertically. Used by the animator.
   *
   * @param maskOffsetY Vertical translation offset of the mask
   */
  private void setMaskOffsetY(int maskOffsetY) {
    if (mMaskOffsetY == maskOffsetY) {
      return;
    }
    mMaskOffsetY = maskOffsetY;
    invalidate(); // 使其重绘
  }

在dispatchDraw处理绘制工作

@Override
  protected void dispatchDraw(Canvas canvas) {
    if (!mAnimationStarted || getWidth() <= 0 || getHeight() <= 0) {
      super.dispatchDraw(canvas);
      return;
    }
    dispatchDrawUsingBitmap(canvas);
  }

  /**
   * Draws and masks the children using a Bitmap.
   *
   * @param canvas Canvas that the masked children will end up being drawn to.
   */
  private boolean dispatchDrawUsingBitmap(Canvas canvas) {
    Bitmap unmaskBitmap = tryObtainRenderUnmaskBitmap();
    Bitmap maskBitmap = tryObtainRenderMaskBitmap();
    if (unmaskBitmap == null || maskBitmap == null) {
      return false;
    }
    // First draw a desaturated version
    drawUnmasked(new Canvas(unmaskBitmap));
    canvas.drawBitmap(unmaskBitmap, 0, 0, mAlphaPaint);

    // Then draw the masked version
    drawMasked(new Canvas(maskBitmap));
    canvas.drawBitmap(maskBitmap, 0, 0, null);

    return true;
  }

最主要的是对Bitmap的处理

/**
   * Draws and masks the children using a Bitmap.
   *
   * @param canvas Canvas that the masked children will end up being drawn to.
   */
  private boolean dispatchDrawUsingBitmap(Canvas canvas) {
    Bitmap unmaskBitmap = tryObtainRenderUnmaskBitmap();    // 获取一个Bitmap,
    Bitmap maskBitmap = tryObtainRenderMaskBitmap();        // 获取一个Bitmap,
    if (unmaskBitmap == null || maskBitmap == null) {
      return false;
    }
    // First draw a desaturated version
    drawUnmasked(new Canvas(unmaskBitmap));     // 将子控件绘制到临时的画布上
    canvas.drawBitmap(unmaskBitmap, 0, 0, mAlphaPaint);// 将子控件绘制到屏幕上

    // Then draw the masked version
    drawMasked(new Canvas(maskBitmap));
    canvas.drawBitmap(maskBitmap, 0, 0, null);

    return true;
  }
  // 试图获取一个Bitmap
  private Bitmap tryObtainRenderUnmaskBitmap() {
    if (mRenderUnmaskBitmap == null) {
      mRenderUnmaskBitmap = tryCreateRenderBitmap();
    }
    return mRenderUnmaskBitmap;
  }

  // 试图获取一个Bitmap
  private Bitmap tryObtainRenderMaskBitmap() {
    if (mRenderMaskBitmap == null) {
      mRenderMaskBitmap = tryCreateRenderBitmap();
    }
    return mRenderMaskBitmap;
  }

  // 其实就是创建一个和自己一样大的Bitmap
  private Bitmap tryCreateRenderBitmap() {
    int width = getWidth();
    int height = getHeight();
    try {
      return createBitmapAndGcIfNecessary(width, height);
    } catch (OutOfMemoryError e) {
      // 打印log
      String logMessage = "ShimmerFrameLayout failed to create working bitmap";
      StringBuilder logMessageStringBuilder = new StringBuilder(logMessage);
      logMessageStringBuilder.append(" (width = ");
      logMessageStringBuilder.append(width);
      logMessageStringBuilder.append(", height = ");
      logMessageStringBuilder.append(height);
      logMessageStringBuilder.append(")\n\n");
      for (StackTraceElement stackTraceElement :
          Thread.currentThread().getStackTrace()) {
        logMessageStringBuilder.append(stackTraceElement.toString());
        logMessageStringBuilder.append("\n");
      }
      logMessage = logMessageStringBuilder.toString();
      Log.d(TAG, logMessage);
    }
    return null;
  }

  // Draws the children without any mask.
  // 将子控件绘制到画布上
  private void drawUnmasked(Canvas renderCanvas) {
    renderCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
    super.dispatchDraw(renderCanvas);
  }

  // Draws the children and masks them on the given Canvas.
  private void drawMasked(Canvas renderCanvas) {
    Bitmap maskBitmap = getMaskBitmap();
    if (maskBitmap == null) {
      return;
    }

    renderCanvas.clipRect(
        mMaskOffsetX,
        mMaskOffsetY,
        mMaskOffsetX + maskBitmap.getWidth(),
        mMaskOffsetY + maskBitmap.getHeight());
    renderCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
    super.dispatchDraw(renderCanvas);

    renderCanvas.drawBitmap(maskBitmap, mMaskOffsetX, mMaskOffsetY, mMaskPaint);
  }

  // Return the mask bitmap, creating it if necessary.
  private Bitmap getMaskBitmap() {
    if (mMaskBitmap != null) {
      return mMaskBitmap;
    }

    int width = mMask.maskWidth(getWidth());
    int height = mMask.maskHeight(getHeight());

    mMaskBitmap = createBitmapAndGcIfNecessary(width, height);
    Canvas canvas = new Canvas(mMaskBitmap);
    Shader gradient;
    switch (mMask.shape) {
      default:
      case LINEAR: {
        int x1, y1;
        int x2, y2;
        switch (mMask.angle) {
          default:
          case CW_0:
            x1 = 0;
            y1 = 0;
            x2 = width;
            y2 = 0;
            break;
          case CW_90:
            x1 = 0;
            y1 = 0;
            x2 = 0;
            y2 = height;
            break;
          case CW_180:
            x1 = width;
            y1 = 0;
            x2 = 0;
            y2 = 0;
            break;
          case CW_270:
            x1 = 0;
            y1 = height;
            x2 = 0;
            y2 = 0;
            break;
        }
        gradient =
            new LinearGradient(
                x1, y1,
                x2, y2,
                mMask.getGradientColors(),
                mMask.getGradientPositions(),
                Shader.TileMode.REPEAT);
        break;
      }
      case RADIAL: {
        int x = width / 2;
        int y = height / 2;
        gradient =
            new RadialGradient(
                x,
                y,
                (float) (Math.max(width, height) / Math.sqrt(2)),
                mMask.getGradientColors(),
                mMask.getGradientPositions(),
                Shader.TileMode.REPEAT);
        break;
      }
    }
    canvas.rotate(mMask.tilt, width / 2, height / 2);
    Paint paint = new Paint();
    paint.setShader(gradient);
    // We need to increase the rect size to account for the tilt
    int padding = (int) (Math.sqrt(2) * Math.max(width, height)) / 2;
    canvas.drawRect(-padding, -padding, width + padding, height + padding, paint);

    return mMaskBitmap;
  }