Android View相关知识点以及原理(四)

本节主要围绕View滑动冲突和View相关的工作原理进行

本系列文章都是先列知识点、流程图、部分简要介绍然后对该内容中会出现在面试中的点进行梳理

有关知识点并没有深入讲解,对于原理流程图部分,强烈推荐配合源码查看

AOSP Android Studio 导入Android源码 (一)

引入:今天群里讨论一个问题,不知道大家是否清楚

问题:在ActivityThread的main方法中loop一直是死循环,我们的生命周期都是通过系统的Handler的回调方法中进行的,包括更新界面,那在代码中执行setText方法或者view的重绘也是在handler的handleMessage方法中执行的吗?是不是所有的关于UI的更新都是走的Android的消息机制通过系统handler更新,下面选取了五个对该问题的讨论

  1. 首先UI刷新走的是ViewRootImpl方法,最终通过WMS刷新,同时ViewRootImpl里面有handler,这个handler是对应主线程的,WMS完成刷新回到ViewRootImpl,此时Handler再帮忙切换到主线程刷新

  2. 屏幕每秒有60帧的刷新,这个是依赖屏幕的刷新频率,每一帧的绘制会通过handler通知,只要屏幕亮着就不存在死循环

  3. 总得来说,安卓系统的显示原理,是安卓程序把经过测量布局绘制后的surface缓存数据,通过surfaceFlinger把数据渲染到显示屏,通过安卓的刷新机制来刷新数据,也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕

  4. view的创建和绘制在onResume里,所以onCreate时拿不到view的宽高,可以通过onWindowFocusChanged和view.post或者ViewTreeObserver获取

  5. setText源码分析,该方法会重绘,从调用链来看,会将视图事件包装成一个Runnable,添加到MessageQueue中,Looper轮询取出,Handler#handleMessage接受到绘制事件回调,并调用ViewRootImpl的performTraversals完成最终绘制

一、View的基础知识

  1. 公号每日一题作者对Android应用坐标系统的全面详解

  2. 对于坐标系需要搞清楚getX()和getRawX()的区别,前者是相对于当前View的左上角的x坐标,后者是相对于整个屏幕

  3. TouchSlop是系统所能识别的被认为是滑动的最小距离,如果滑动距离小于这个常量,系统将认为你没有滑动。通过ViewConfiguration.get(getContext()).getScaledTouchSlop()得到,这个常量处于frameworks/base/core/res/res/values/config.xml中,我们可以利用这个常量来过滤一些操作来提升用户体验

  4. VelocityTracker是一个速度追踪类,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。例如想获取1秒内手指移动的速度

    VelocityTracker velocityTracker = VelocityTracker.obtain();
    velocityTracker.addMovement(event);
    velocityTracker.computeCurrentVelocity(1000);//1000毫秒内
    int xVelocity = (int)velocityTracker.getXVelocity();
    int yVelocity = (int)velocityTracker.getYVelocity();
    //回收
    velocityTracker.clear();
    velocityTracker.recycle();
  5. GestureDetector类是用来进行手势检测的,例如用户单击、滑动、长按、双击等等。一般只需要实现相应的监听事件,接管View的onTouchEvent就可以正常工作了。

  6. View的scrollBy和scrollTo区别,前者最终也是调用后者,scrollTo属于绝对滑动,以(0,0)坐标为基准,scrollBy属于基于当前位置的相对滑动,具体查看View中关于这两个方法的源码即可。scrollTo/scrollBy相关介绍

  7. View的三种滑动方式,scrollTo/scrollBy操作简单,适合对View内容的滑动;动画,操作简单,主要适用于没有交互的View和实现复杂的动画效果;改变布局参数,操作复杂点,适用于有交互的View。

  8. 弹性滑动相关

    • 利用Scroller类来实现有动画效果的滑动,以下是惯用代码

      Scroller scroller = new Scroller(mContext); 
      ​
      private void smoothScrollTo(int dstX, int dstY) {
        int scrollX = getScrollX();
        int scrollY = getScrollY();
        int deltaX = dstX - scrollX;
        int deltaY = dstY - scrollY;
        scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //开始滑动
        invalidate(); 
      }
      ​
      @Override
      public void computeScroll() {
        if (scroller.computeScrollOffset()) { 
          scrollTo(scroller.getCurrX(), scroller.getCurY());
          postInvalidate();
        }
      }
      ​
    • 对上述代码解释:当执行了invalidate()后,会导致View重绘,view的draw方法中又会去调用computeScroll方法,computeScroll方法又会去向Scroller获取当前的scrollX和scrollY,再通过scrollTo方法滑动,然后调用postInvalidate()又会导致view重绘,以此循环,直到滑动过程结束。

    • 当然通过动画也可以实现弹性滑动,利用ObjectAnimator类,这里不过多介绍,详细可以用BING搜索

    • 还可以通过延时策略,主要是利用Handler的postDelayed方法,每次移动一点。

二、View的事件分发机制

  1. 首先可以了解一下View是怎么显示在界面上的 Activity和VIEW以及Window的关联

  2. 一个常见的图

     

     

  3. 点击事件的传递顺序 Activity -> Window -> DecorView,事件先传递给Activity,然后再传递给Window,然后再传递给顶级View,顶级View接受到事件后,按照事件分发机制去分发事件。

  4. 下面这是自己画的图片

     

  5. 事件分发过程可以查看该图片,建议先把源码看一遍,然后对该图会有比较深的印象。

    • 首先会在Activity中调用dispatchTouchEvent分发,然后再调用ViewGroup的dispatchTouchEvent分发,如果ViewGroup调用onInterceptTouchEvent拦截,这个事件就会交给ViewGroup的onTouchEvent处理,如果不拦截,那么该事件将会传递给子类,子类的dispatchTouchEvent会被调用,只要有一个子类消费了此事件,事件传递就结束了

    • 它们的关系可以通过以下伪代码表示

      public boolean dispatchTouchEvent(MotionEvent ev){
          boolean consume = false;
          if(onInterceptTouchEvent(ev)){
              consume = onTouchEvent(ev);
          }else{
              consume = child.dispatchTouchEvent(ev);
          }
          return consume;
      }
    • 需要注意的是,在View的dispatchTouchEvent中有如下代码,从代码中可以看出,设置了onTouch监听事件,返回true将不会执行onTouchEvent方法了,所以一般监听事件内都默认了返回false。并且在onTouchEvent内,会执行onClickListener事件。

           if (li != null && li.mOnTouchListener != null
                          && (mViewFlags & ENABLED_MASK) == ENABLED
                          && li.mOnTouchListener.onTouch(this, event)) {
                      result = true;
                  }
      ​
                  if (!result && onTouchEvent(event)) {
                      result = true;
                  }
    • 在ViewGroup的dispatchTouch代码中有如下代码,其中resetTouchState是重置FLAG_DISALLOW_INTERCEPT标记位的,一般用于子View当中,这里这么做的效果是,当面对ACTION_DOWN事件时,ViewGroup总是会调用onInterceptTouchEvent来询问自己是否要拦截事件。如果ViewGroup拦截事件的话,后续所有事件将会交由它处理,不再调用onInterceptTouchEvent。

          @Override
          public boolean dispatchTouchEvent(MotionEvent ev) {
                  ........
                  // Handle an initial down.
                  if (actionMasked == MotionEvent.ACTION_DOWN) {
                      // Throw away all previous state when starting a new touch gesture.
                      // The framework may have dropped the up or cancel event for the previous gesture
                      // due to an app switch, ANR, or some other state change.
                      cancelAndClearTouchTargets(ev);
                      resetTouchState();
                  }
                        // Check for interception.
                  final boolean intercepted;
                  if (actionMasked == MotionEvent.ACTION_DOWN
                          || mFirstTouchTarget != null) {
                      final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                      if (!disallowIntercept) {
                          intercepted = onInterceptTouchEvent(ev);
                          ev.setAction(action); // restore action in case it was changed
                      } else {
                          intercepted = false;
                      }
                  } else {
                      // There are no touch targets and this action is not an initial down
                      // so this view group continues to intercept touches.
                      intercepted = true;
                  }
                  ........

三、滑动冲突

  1. 常见的滑动冲突场景

    • 外部滑动方向和内部滑动方向不一致

    • 外部滑动方向和内部滑动方向一致

    • 上面两种情况的嵌套

  2. 解决办法有外部拦截法和内部拦截法

    • 这里对上述场景的第一点进行分析,例如ViewPager + ListView场景,这里ViewPager已经帮我们处理好了,所以接下来需要模拟一个左右滑动的控件HorizontalScrollViewEx

    • 外部拦截法,只需要在HorizontalScrollViewEx内,重写onInterceptTouchEvent方法,需要注意的是,不能屏蔽ACTION_DOWN方法,从源码中可以看出,一旦屏蔽了该事件,后续的ACTION_UP等事件都会直接交由自己处理,传递不到子元素。代码如下

      //重写父容器HorizontalScrollViewEx的拦截方法
      public boolean onInterceptTouchEvent (MotionEvent event){
          boolean intercepted = false;
          int x = (int) event.getX();
          int y = (int) event.getY();
          switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN://对于ACTION_DOWN事件必须返回false,一旦拦截后续事件将不能传递给子View
               intercepted = false;
               break;
            case MotionEvent.ACTION_MOVE://对于ACTION_MOVE事件根据需要决定是否拦截
               if (父容器需要当前事件) {
                   intercepted = true;
               } else {
                   intercepted = flase;
               }
               break;
         }
            case MotionEvent.ACTION_UP://onClick事件在UP事件中调用,一旦拦截子View的onClick事件将不会触发
               intercepted = false;
               break;
            default : break;
         }
          mLastXIntercept = x;
          mLastYIntercept = y;
          return intercepted;
         }
      ​
    • 内部拦截法,这个比较复杂点,一般能用外部解决还是用外部解决吧。这个方法利用到了FLAG_DISALLOW_INTERCEPT标记位,具体代码如下

      //重写父类HorizontalScrollViewEx的onInterceptTouchEvent方法
      public boolean onInterceptTouchEvent (MotionEvent event) {
       int action = event.getAction();
       if(action == MotionEvent.ACTION_DOWN) {
           return false;
       } else {
           return true;
       }
      }
      ​
      //重写子类ListView的dispatchTouchEvent方法
      public boolean dispatchTouchEvent ( MotionEvent event ) {
        int x = (int) event.getX();
        int y = (int) event.getY();
      ​
        switch (event.getAction) {
            case MotionEvent.ACTION_DOWN:
               parent.requestDisallowInterceptTouchEvent(true);//为true表示禁止父容器拦截
               break;
            case MotionEvent.ACTION_MOVE:
               int deltaX = x - mLastX;
               int deltaY = y - mLastY;
               if (父容器需要此类点击事件) {
                   parent.requestDisallowInterceptTouchEvent(false);
               }
               break;
            case MotionEvent.ACTION_UP:
               break;
            default :
               break;        
       }
      ​
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
      }
      ​

四、View的工作原理

  1. 在Android 8.0源码的handlerResumeActivity方法内performResumeActivity后会执行将Activity和DecorView和Window关联起来,其中ViewRoot的实现类是ViewRootImpl,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。

  2. View的绘制是从ViewRoot的performTraversal方法开始的,它经过measure、layout、和draw三个过程最终将一个View绘制出来的,源码内调用流程如下图

     

  3. measure过程决定了View的宽高,Measure完成后,可以通过getMeasureWidth和getMeasureHeight方法来获取测量后的宽高,获取的时机最后是在onResume中或者通过开头回答部分的三种方法。该measure过程,就是反复遍历整个View数进行测量。

    • measure过程中会遇到MeasureSpec,它代表了一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(测量模式下的规格大小)。其中SpecMode有三类,如以下描述

      • UNSPECIFIED 父容器不对View限制,要多大有多大

      • EXACTLY 精确大小,为SpecSize值,一般对应于match_parent和具体数据两种模式

      • AT_MOST 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值

    • 在源码performMeasure前,调用了getRootMeasureSpec,然后再将结果作为参数传递给performMeasure进行测量,这就是DecorView的创建过程之一,

          private static int getRootMeasureSpec(int windowSize, int rootDimension) {
              int measureSpec;
              switch (rootDimension) {
      ​
              case ViewGroup.LayoutParams.MATCH_PARENT:
                  // Window can't resize. Force root view to be windowSize.
                  measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                  break;
              case ViewGroup.LayoutParams.WRAP_CONTENT:
                  // Window can resize. Set max size for root view.
                  measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                  break;
              default:
                  // Window wants to be an exact size. Force root view to be that size.
                  measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                  break;
              }
              return measureSpec;
          }
    • 对于普通的View来说,一般就是指ViewGroup,它调用getChildMeasureSpce得到子元素的MeasureSpec,与父容器的Measure和子元素本身的LayoutParams有关,这就是为什么自定义View时使用了warp_content,最后却还是布满整个屏幕的原因

          public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
              int specMode = MeasureSpec.getMode(spec);
              int specSize = MeasureSpec.getSize(spec);
      ​
              int size = Math.max(0, specSize - padding);
      ​
              int resultSize = 0;
              int resultMode = 0;
      ​
              switch (specMode) {
              // Parent has imposed an exact size on us
              case MeasureSpec.EXACTLY:
                  if (childDimension >= 0) {
                      resultSize = childDimension;
                      resultMode = MeasureSpec.EXACTLY;
                  } else if (childDimension == LayoutParams.MATCH_PARENT) {
                      // Child wants to be our size. So be it.
                      resultSize = size;
                      resultMode = MeasureSpec.EXACTLY;
                  } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                      // Child wants to determine its own size. It can't be
                      // bigger than us.
                      resultSize = size;
                      resultMode = MeasureSpec.AT_MOST;
                  }
                  break;
      ​
              // Parent has imposed a maximum size on us
              case MeasureSpec.AT_MOST:
                  if (childDimension >= 0) {
                      // Child wants a specific size... so be it
                      resultSize = childDimension;
                      resultMode = MeasureSpec.EXACTLY;
                  } else if (childDimension == LayoutParams.MATCH_PARENT) {
                      // Child wants to be our size, but our size is not fixed.
                      // Constrain child to not be bigger than us.
                      resultSize = size;
                      resultMode = MeasureSpec.AT_MOST;
                  } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                      // Child wants to determine its own size. It can't be
                      // bigger than us.
                      resultSize = size;
                      resultMode = MeasureSpec.AT_MOST;
                  }
                  break;
      ​
              // Parent asked to see how big we want to be
              case MeasureSpec.UNSPECIFIED:
                  if (childDimension >= 0) {
                      // Child wants a specific size... let him have it
                      resultSize = childDimension;
                      resultMode = MeasureSpec.EXACTLY;
                  } else if (childDimension == LayoutParams.MATCH_PARENT) {
                      // Child wants to be our size... find out how big it should
                      // be
                      resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                      resultMode = MeasureSpec.UNSPECIFIED;
                  } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                      // Child wants to determine its own size.... find out how
                      // big it should be
                      resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                      resultMode = MeasureSpec.UNSPECIFIED;
                  }
                  break;
              }
              //noinspection ResourceType
              return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
          }
    • 从上面两点可以得出普通View的MeasureSpce规则,结论如下表

       

       

    • 从上表中可以知道,如果子View使用wrap_content,那么会遇到宽度直接是match_parent,所以需要重写onMeasure方法,(详细查看自定义View warp_content不起作用的原因)模板如下

      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          super.onMeasure(widthMeasureSpec,heightMeasureSpec);
              
          int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
          int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
          int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
          int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
          //根据不同的模式来设置
          if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
              setMeasuredDimension(mWidth,mHeight);
          }else if(widthSpecMode == MeasureSpec.AT_MOST){
              setMeasuredDimension(mWidth,heightSpecSize);
          }else if(heightSpecMode == MeasureSpec.AT_MOST){
              setMeasuredDimension(widthSpecSize,mHeight);
          }
      }
      ​
    • ViewGroup内并没有定义measure()具体测量的过程,因为ViewGroup布局特性不同,例如LinearLayout和RelativeLayout,显然处理方式不一样,layout()方法也一样并没有实现

  4. 当ViewGroup位置确定后,它在onLayout中会遍历所有的子元素,调用其layout方法,以下是View的layout方法源码

     public void layout(int l, int t, int r, int b) {
            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;
    ​
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);//这里会利用setFrame初始化View的四个顶点
    ​
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);//该方法并未实现,比如线性布局和相对布局一样,特性不同,子View也不定
    ​
                if (shouldDrawRoundScrollbar()) {
                    if(mRoundScrollbarRenderer == null) {
                        mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                    }
                } else {
                    mRoundScrollbarRenderer = null;
                }
    ​
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    ​
                ListenerInfo li = mListenerInfo;
                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) {
                        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);
            }
        }
  5. draw过程,首先调用drawBakcground绘制背景,然后调用onDraw()方法绘制自己,再调用dispatchDraw()方法绘制Children,最后调用onDrawScrollBars()绘制装饰,通过源码很容易查看出来。

五、自定义View相关

  1. 自定义的分类

    • 继承View重写onDraw方法,对于warp_content和padding需要自己处理

    • 继承ViewGroup派生特殊的layout,需要同理子元素的测量和布局过程

    • 继承特定的View(如TextView),大多数情况下不需要重写onDraw方法

    • 继承特定的ViewGrou(如LinearLayout),和第二点类似,只不过第二点更接近View的底层

  2. 比如继承View画圆

    //方法一
    public void onDraw(Canvas canvas){
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width,height)/2;
        canvas.drawCircle(width/2,height/2,radius,mPaint);
    }
    //方法二
    public void onDraw(Canvas canvas){
        super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width,height)/2;
        canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint);
    }

    上述两种方法都可以画圆,当宽高属性都设置为match_parent时,效果一样,但是加上了padding后,例如设置paddingTop为20dp,在方法一的处理过程中就失效了。当宽高属性设置为wrap_content时,就需要使用下面模板了,这里的mWidth和mHeight设置为200dp

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
            
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //根据不同的模式来设置
        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,mHeight);
        }
    }
  3. 继承ViewGroup派生特殊Layout,以下是重写onMeasure、onLayout简单布局模板,其实onMeasure()可以借鉴measureChildren处理方式

    //onMeasure
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        int measureWidth = 0;
        int measureHeight = 0;
        int childCount = getChildCount();
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        
        if(childCount == 0){
            setMeasureDimension(0,0);
        }else if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            View childView = getChildAt(0);//拿到子View,或者遍历处理子View
            measureWidth = ....;
            measureHeight = ....;
            setMeasuredDimension(measureWidth,measureHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            measureWidth = ....;
            setMeasuredDimension(measureWidth,heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            measureHeight = ....;
            setMeasuredDimension(widthSpecSize,measureHeight);
        }
    }
    ​
    //onLayout
    protected void onLayout(boolean changed,int l,int t,int r,int b){
        int childCount = getChildCount();
        
        for(int i = 0;i<childCount;i++){
            View childView = getChildAt(i);
            if(childView.getVisibility() != View.GONE){
                ....处理各种padding margin
                childView.layout(childLeft,childTop,childRight,childBottom);
            }
        }
    }
    ​

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值