小试牛刀-onLayout方法

  onLayout方法是ViewGroup中的一个抽象方法,继承ViewGroup的时候必须重写该方法,该方法是用于对子View进行布局的。

  和onMeasure方法一样,onLayout也有个对应的layout方法。先看下layout和onLayout的区别

  • layout方法是View中的方法,用来实现View的摆放,onLayout方法是ViewGroup中的方法,用来实现子View的摆放
  • layout传入是个参数left、top、right、bottom是相对于父控件而言的,例如传入(20,20,50,70),该View的左上角位置对应的坐标是(20,20),宽为30,高为50;onLayout传入的参数l、t、r、f是更具父控件的实际可用空间来的(去除margin和padding的控件)

先看下layout方法

View中的layout方法

   public void layout(int l, int t, int r, int b) {
      //进行判断是否要调用onMeasure方法(所以onMeasure会被调用不止一次)
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
     /**先判断isLayoutModeOptical方法返回结果,如果是true就执行        
        setOpticalFrame方法,如果是false就执行setFrame方法。
        setOpticalFrame方法返回结果是执行setFrame方法返回的布尔值。  
        所以不管执行那个方法都会只想setFrame方法。
        setFrame方法将新的left、top、right、bottom存储到View的成员变量中。然后根据返回的布尔值确认View的尺寸或者位置是否发生变化。
**/
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
      //如果View的尺寸或者位置发生变化或者mPrivateFlags等于layout的标签PFLAG_LAYOUT_REQUIRED就执行以下代码
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
      //最先执行,在View中的onLayout是一个空实现的方法,用来实现View的摆放;在ViewGroup中onLayout是一个抽象方法,子类必须实现,调用layout方法循环摆放所有子View的位置
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }
        //执行完onLayout方法后,从mPrivateFlags中移除标签PFLAG_LAYOUT_REQUIRED
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            //存储各种监听器
            ListenerInfo li = mListenerInfo;
          //通过View的addOnLayoutChangeListener添加View位置和大小发生变化的监听器,将监听器存储在ListenerInfo中的mOnLayoutChangeListeners的集合中
            if (li != null && li.mOnLayoutChangeListeners != null) {
        //拷贝所有监听器
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
        //遍历调用onLayoutChange方法,View的监听事件就得到了响应
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }




    private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }


   protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

View的layout方法
  这是一个被final修饰的方法,意味着无法被子类重写。但是内部事件还是调用了View中的layout方法

 @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

再看下onLayout方法

  View中的onLayout是一个空实现的方法,通过layout方法将新的left、top、right、bottom传给onLayout。
  官方注释在此视图应该时从布局调用(layout方法执行),此视图(onLayout方法执行)给每个孩子分配一个大小和位置。子类的派生类应该重写此方法并在每个子View上调用布局(layout方法)。
  View的子类当然是ViewGroup了,所以ViewGroup更加像是一个View的管理器,用来实现对子View的大小和位置变化进行控制。简单来说View会通过onLayout方法进行确认View的显示位置。

    /**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * 当这个view和其子view被分配一个大小和位置时,被layout调用。
     * @param changed 当前View的大小和位置改变了
     * @param left 左部位置(相对于父视图)
     * @param top 顶部位置(相对于父视图)
     * @param right 右部位置(相对于父视图)
     * @param bottom 底部位置(相对于父视图)
     *
     */
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

  ViewGroup中的onLayout方法是一个抽象方法,所以子类必须实现。并且调用View中的layout方法来确认View的位置和大小。

   @Override
    protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

如何获取摆放子View后的位置

  在layout方法中mLeft、mTop、mRight、mBottom这四个值是用来进行对View位置的摆放和大小的限制,是相对于父控件而言的。

如图白色区域是父View,黑色区域为子View。

  • mLeft = view.getLeft()  子View左边界到父view左边界的距离
  • mTop = view. getTop()  子View上边界到父view上边界的距离
  • mRight = view. getRight()  子View右边界到父view右边界的距离
  • mBottom = view. getBottom()  子View下边界到父view下边界的距离
   public final int getLeft() {
        return mLeft;
    }
  • view.getWidth() View的宽度,子View的右边界 - 子view的左边界
  public final int getWidth() {
        return mRight - mLeft;
    }
  • view.getWidth() View的高度,子View的下边界 - 子view的上边界
   public final int getHeight() {
        return mBottom - mTop;
    }
  • view.getMeasuredWidth() measure过程中返回的mMeasuredWidth
   public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
  • view.getMeasuredHeight() measure过程中返回的mMeasuredHeight
  public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }

getWidth()/Height和getMeasuredWidth()/getMeasuredHeight的区别?

针对getWidth()和getMeasuredWidth()进行分析,先看下区别

  • getMeasuredWidth()是在measure()结束后得到的值,getWidth()是在layout()结束后得到的值
  • getMeasureWidth()是通过setMeasuredDimension()方法来设置的,即getWidth()是通过视图右边的坐标减去左边的坐标计算出来的

分析下getWidth()和getMeasuredWidth()是如何赋值的

   public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

  mMeasuredWidth这个值之前在小试牛刀-onMeasure方法中介绍过是View的实际大小宽对应的值是mMeasuredWidth,而mMeasuredWidth是通过setMeasuredDimension方法设置进来的。所以在measure方法结束后mMeasuredWidth才会有值,此时调用getMeasuredWidth可以获取对应的值。

  public final int getWidth() {
        return mRight - mLeft;
    }

  在getWidth()中是通过mRight - mLeft的计算返回的结果。而mRight和mLeft这两个值,上面有介绍是通过layout传递过来的。

简单写了个Demo

Demo
public class CustomViewGroup extends ViewGroup {
  private static final String TAG = "CustomViewGroup";
  int startX = 0;
  int startY = 0;
  boolean isMove = false;

  public CustomViewGroup(Context context) {
    this(context, null);
  }

  public CustomViewGroup(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

  @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    //20
    Log.e(TAG, "getMeasuredWidth: " + getChildAt(0).getMeasuredWidth());
  }

  @SuppressLint("DrawAllocation") @Override
  protected void onLayout(boolean changed, final int l, int t, int r, int b) {
    View view1 = getChildAt(0);
    View view2 = getChildAt(1);
    view1.setBackgroundColor(getResources().getColor(R.color.colorAccent));
    view2.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
    view1.layout(l, t, 200, 200);
    view1.measure(0, 0);
    view2.layout(view1.getWidth(), view1.getHeight(), view1.getWidth() + 200,
        view1.getHeight() + 200);
    initListener(view1);
    initListener(view2);
  }

  @SuppressLint("ClickableViewAccessibility") private void initListener(final View view) {
    view.setOnClickListener(new OnClickListener() {
      @Override public void onClick(View v) {
        //点击事件生效
      }
    });
    view.setOnTouchListener(new OnTouchListener() {
      @Override public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
          case MotionEvent.ACTION_DOWN:
            startX = (int) event.getX();
            startY = (int) event.getY();
            isMove = false;
            break;
          case MotionEvent.ACTION_MOVE:
            isMove = true;
            int moveX = (int) event.getX() - startX;
            int moveY = (int) event.getY() - startY;
            int left = view.getLeft() + moveX;
            int top = view.getTop() + moveY;
            int right = view.getRight() + moveX;
            int bottom = view.getBottom() + moveY;
            //限定边界
            if (left <= 0) {
              left = 0;
              right = view.getWidth();
            }
            if (right >= getWidth()) {
              left = getWidth() - view.getWidth();
              right = getWidth();
            }
            if (top <= 0) {
              top = 0;
              bottom = view.getHeight();
            }
            if (bottom >= getHeight()) {
              bottom = getHeight();
              top = getHeight() - view.getHeight();
            }
            view.layout(left, top, right, bottom);
            break;
          case MotionEvent.ACTION_UP:
            int l = view.getLeft();
            int t = view.getTop();
            int r = view.getRight();
            int b = view.getBottom();
            if (l <= getWidth() / 2 - view.getWidth() / 2) {
              l = 0;
              r = view.getWidth();
            }
            if (l > getWidth() / 2 - view.getWidth() / 2) {
              r = getWidth();
              l = getWidth() - view.getWidth();
            }
            view.layout(l, t, r, b);
            if (!isMove) {
              view.performClick();
            }
            break;
          default:
        }
        return true;
      }
    });
  }
}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

  <com.test.ui.CustomViewGroup
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      >
    <View
        android:layout_width="20px"
        android:layout_height="wrap_content"/>
    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
  </com.test.ui.CustomViewGroup>

</LinearLayout>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容