美文网首页Android自定义控件
小试牛刀-onLayout方法

小试牛刀-onLayout方法

作者: 同学别闹 | 来源:发表于2018-07-17 15:27 被阅读0次

      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>
    
    

    相关文章

      网友评论

        本文标题:小试牛刀-onLayout方法

        本文链接:https://www.haomeiwen.com/subject/qbgguftx.html