[Android開發(fā)藝術(shù)探索]第四章學(xué)習(xí)筆記

ViewRoot 與 DecorView

ViewRoot 是連接 WindowManager 和 DecorView 的紐帶,其實現(xiàn)類是 ViewRootImpl 類。View 的三大流程均由ViewRoot 完成澜术。Activity 被創(chuàng)建完成后蔫缸,會將 DecorView 添加到 Window 中,同時創(chuàng)建 ViewRootImpl 對象贺嫂,并與DecorView 建立關(guān)聯(lián)滓鸠。

root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams, panelParentView);

View 的繪制流程從 ViewRoot 的 performTraversals 方法開始:

performTraversals 方法

performMeasure、performLayout第喳、performDraw 三個方法分別完成頂級 View 的measure糜俗、layout、draw 這三大流程。其中悠抹,performMeasure 中調(diào)用 measure 方法珠月,measure 中又調(diào)用 onMeasure 方法,onMeasure中會對所有子元素進行 measure 過程楔敌。measure 流程就是這樣從父元素傳遞到子元素中啤挎,子元素會重復(fù)這個過程,從而完成整個 View樹的遍歷卵凑。performLayout庆聘、performDraw 的傳遞流程與此類似(draw方法中通過 dispatchDraw 來實現(xiàn)傳遞過程)。

  • measure 過程決定 View 的寬高勺卢,完成后可通過 getMeasuredWidthgetMeasuredHeight 方法獲取測量后的寬高伙判,一般此寬高就是 View 的最終寬高(特殊情況下例外)。
  • layout 過程決定 View 的四個頂點和實際寬高黑忱,完成后可通過 getTop 宴抚、 getBottongetLeftgetRight 拿到 View 的四個頂點坐標甫煞,且可以通過 getWidth菇曲、getHeight 方法拿到最終寬高。
    *draw 過程決定了 View 的顯示危虱。

DecorView作為頂級View羊娃,其實是一個 FrameLayout ,它包含一個豎直方向的 LinearLayout 埃跷,這個LinearLayout 分為標題欄和內(nèi)容欄兩個部分蕊玷。

DecorView

Activity通過setContextView所設(shè)置的布局文件其實就是被加載到內(nèi)容欄之中的。這個內(nèi)容欄的id是 R.android.id.content 弥雹,通過 ViewGroup content = findViewById(R.android.id.content);可以得到這個contentView垃帅。content.getChildAt(0)可以獲得我們設(shè)置的 View。

MeasureSpec

MeasureSpec 參與 View 的 measure 的過程剪勿,類似于一個測量規(guī)格的概念贸诚。其創(chuàng)建過程受父容器影響。測量過程中厕吉,系統(tǒng)根據(jù) View 的 LayoutParams 根據(jù)父容器所施加規(guī)則轉(zhuǎn)換成對應(yīng)MeasureSpec酱固,根據(jù)這個 measureSpec 得到View 的測量寬高。
MeasureSpec 代表一個 32 位 int 值头朱,高 2 位代表 SpecMode运悲,低 30 位 代表 SpecSize。
MeasureSpec 通過 makeMeasureSpec(int size , int mode) 方法將模式和尺寸打包成一個 int 值项钮,且可以通過getModegetSize 方法解包原始值(此處 MeasureSpec 指其代表 int 值班眯,而非對象本身)希停。

SpecMode有三類:

  • UNSPECIFIED 父容器不對View有限制,要多大給多大署隘,系統(tǒng)內(nèi)部用宠能。
  • EXACTLY 父容器已檢測出 View 所需精確大小。View 最終大小就是 SpecSize 指定值磁餐,對應(yīng)
    match_parent 和具體數(shù)值违崇。
  • AT_MOST 父容器指定了一個可用大小即 SpecSize,View 的大小不能大于這個值崖媚,具體值要看 View 的具體實現(xiàn)亦歉。對應(yīng) wrap_content恤浪。

DecorView 的 MeasureSpec 由窗口尺寸及自身參數(shù)共同決定畅哑。
普通 View 的 MeasureSpec 則由父容器與自身參數(shù)共同決定。

普通 View 的 MeasureSpec 的創(chuàng)建規(guī)則

這里的parentSize 實際上是父容器剩余的可用空間水由,要減去父容器 padding荠呐、自身 margin、和父容器已經(jīng)使用的部分砂客。

View 的工作流程

measure

View 的 measure 過程
View 的 measure 過程
  • getSuggestedMinimumSize() 方法中泥张,若有設(shè)置背景,會獲取背景(Drawable)的原始寬高作為備選的返回值鞠值。Drawable若無原始寬高會默認為0媚创。ShapeDrawable 無原始寬高,BitmapDrawable 有原始寬高(圖片的尺寸)彤恶。

  • 直接繼承 View 的自定義控件需要重寫 onMeasure 方法并設(shè)置 wrap_content 時的自身大小钞钙,否則
    wrap_content 效果等同 matct_parent。原因通過閱讀源碼可知声离,getDefaultSize 方法中只判斷了是否為 UNSPECIFIED 模式芒炼,對EXACTLY 和 AT_MOST 處理是一樣的,即返回 MeasureSpec 的 size 作為 View 的測量寬高术徊。通過View 的 MeasureSpec 的創(chuàng)建規(guī)則表可知本刽,size 為父容器剩余的可用空間。

  //設(shè)置 wrap_content 時的自身大小
  @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    //mWidth, mHeight 為自定義的默認內(nèi)部寬高 
    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 除了完成自身的 measure 過程外赠涮,應(yīng)該去遍歷所有子 View 的measure 方法子寓,各個子元素再去遞歸執(zhí)行這個過程。但是 ViewGroup 是一個抽象類笋除,并沒有重寫 onMeasure 方法斜友。所以自定義 ViewGroup 時應(yīng)該根據(jù)所需重寫 onMeasure 方法。

VIewGroup 雖然沒有重寫 onMeasure 方法株憾,但是相應(yīng)提供了一些自定義時可以直接使用的方法:

  • measureChild() :取出子 View 的LayoutParams 和本身的MeasureSpec 來創(chuàng)建子 View 的 MeasureSpec(通過getChildMeasureSpec 方法)蝙寨,直接傳遞給子 View 的measure 方法進行測量晒衩。

  • measureChildren() :作用是遍歷所有子 View,對所有可見不為 GONE 的 View 使用 measureChild()方法墙歪。直接放到ViewGroup 的onMeasure 中可以實現(xiàn)類似無 wrap_content 功能的 FrameLayout 的效果听系。

  • resolveSizeAndState :主要是根據(jù)期望值(根據(jù)業(yè)務(wù)邏輯在重寫的 onMeasure 中計算的尺寸)和ViewGroup 的 MeasureSpec 的模式和尺寸,判斷最終返回的測量尺寸虹菲,一般用于重寫 onMeasure 方法最后 setMeasureDimension 的參數(shù)處理靠胜。防止出現(xiàn)超過父容器剩余空間等情況。

一般重寫 ViewGroup 的onMeasure 方法的思路是毕源,遍歷子元素進行 measure浪漠,按需求保存某方向尺寸的累加值或者最大值,加上padding霎褐、子View 的 margin等址愿。最終使用這個值設(shè)定 VIewGroup 的測量尺寸。

具體實現(xiàn)可以看看這篇文章冻璃,自定義ViewGroup响谓。

獲取 View 的寬高

measure 過程完成后,理論上可以通過 getMeasureWidth/Height 方法獲取 View 的測量寬高省艳,但是極端情況下娘纷,系統(tǒng)可能需要多次 measure 才能確定最終測量寬高,這種情況下獲取數(shù)值可能出錯跋炕。

應(yīng)該在 onLayout 方法中去獲取 VIew 的測量寬高或者最終寬高赖晶。

由于 View 的 measure 過程和 Activity 的生命周期并不是同步執(zhí)行,無法保證在Activity的 onCreate辐烂、onStart遏插、onResume 時某個View就已經(jīng)測量完畢。想要在 Activity 啟動的時候就獲取一個View的寬高的方法有以下 4 種:

  • Activity / View # onWindowFocusChanged :這個方法的含義是:View 已經(jīng)初始化完畢了棉圈,寬高已經(jīng)準備好了涩堤,需要注意:它會被調(diào)用多次,當 Activity 的窗口得到焦點和失去焦點均會被調(diào)用分瘾。
  • view.post(runnable) :通過 post 將一個 runnable 投遞到消息隊列的尾部胎围,當 Looper 調(diào)用此 runnable 的時候,View 也初始化好了德召。
  • ViewTreeObserver : 使用 ViewTreeObserver 的眾多回調(diào)可以完成這個功能白魂,比如 OnGlobalLayoutListener 這個接口,當 View 樹的狀態(tài)發(fā)送改變或 View 樹內(nèi)部的 View 的可見性發(fā)生改變時上岗,onGlobalLayout 方法會被回調(diào)福荸,這是獲取 View 寬高的好時機。需要注意的是肴掷,伴隨著 View 樹狀態(tài)的改變敬锐, onGlobalLayout 會被回調(diào)多次背传。
  • view.measure(int widthMeasureSpec,int heightMeasureSpec) :手動對view進行measure。需要根據(jù)View的layoutParams分情況處理台夺。
    為 match_parent 時径玖,無法 measure 出具體的寬高,因為不知道父容器的剩余空間颤介,無法測量出 View 的大小梳星。
    為具體的數(shù)值時( dp/px):
  //假設(shè)寬高為 100
  int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
  int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
  view.measure(widthMeasureSpec,heightMeasureSpec);

為 wrap_content 時:

  int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
  // View的尺寸使用30位二進制表示,最大值30個1滚朵,在AT_MOST模式下冤灾,我們用View理論上能支持的最大
  //值去構(gòu)造MeasureSpec是合理的
  int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
  view.measure(widthMeasureSpec,heightMeasureSpec);

layout

layout 過程比較簡單,layout 的方法大致流程是:setFrame 方法設(shè)定 4 個頂點位置(初始化 mLeft辕近、mRight韵吨、mTop、mBottom 4 個值)亏推,然后調(diào)用 onLayout 方法(View 中為空實現(xiàn)学赛、ViewGroup 為抽象方法)年堆,基本實現(xiàn)思路是:遍歷所有子 View吞杭,獲取其測量寬高,和根據(jù)需求計算其中兩個頂點变丧,根據(jù)這 4 個值對子 View 使用 layout 方法芽狗。

getWidth/Height 與getMeasureWidth/Height 方法的區(qū)別
public final int getHeight() {
    return mBottom - mTop;
  }

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

在View 的默認實現(xiàn)中,VIew 的測量寬高等于最終寬高痒蓬,但是測量寬高的形成時間先于最終寬高童擎。在日常開發(fā)中,一般來說兩者相等攻晒。但是存在特殊情況導(dǎo)致兩者不等顾复。如在 layout 方法中操作 setFrame 方法參數(shù)。另外一些情況下鲁捏,View 需要多次 measure 才能確定自己的測量寬高芯砸,這會導(dǎo)致在前幾次測量之后得出的測量寬高與最終寬高不等。

draw

比較簡單大致流程如下:

  • 繪制背景 background.draw(canvas)
  • 繪制自己(onDraw)
  • 繪制 children (dispatchDraw)
  • 繪制裝飾 (onDrawScrollBars)

需要注意兩個方法 :
dispatchDraw 方法會遍歷調(diào)用所有子元素的 draw 方法给梅。
setWillNotDraw 操作一個標記位假丧,在 View 默認為 false,ViewGroup 中默認為 true动羽。作用是View本身不需要繪制任何內(nèi)容時(繪制自己包帚,即 onDraw 方法沒干啥事),系統(tǒng)會進行相應(yīng)優(yōu)化运吓。所以渴邦,當一個 ViewGroup 需要通過 onDraw 來繪制內(nèi)容時(重寫了 onDraw 方法時)疯趟,需要顯示的關(guān)閉該標記位。

自定義View

自定義 View 的分類與須知

自定義 VIew 的 4 類:

  • 繼承View 重寫 onDraw 方法:主要用于實現(xiàn)不規(guī)則效果谋梭,往往需要靜態(tài)或動態(tài)繪制一些不規(guī)則圖形迅办。需要自己實現(xiàn)支持 wrap_content 與 padding。
  • 繼承 ViewGroup 派生特殊的 Layout:用于實現(xiàn)自定義布局章蚣,實現(xiàn)比較復(fù)雜站欺,需要處理自身和子元素的 measure、layout 兩個過程纤垂。
  • 繼承特定 View:較常見做法矾策,一般用于擴展已有 View 的功能。不需要自己實現(xiàn)支持 wrap_content 與 padding峭沦。
  • 繼承特定 ViewGroup:較常見做法贾虽,用于實現(xiàn) 多種 View 組合的效果。不需自己處理自身和子元素的 measure吼鱼、layout 蓬豁。

自定義 View 須知:

  • 直接繼承 View 或者 ViewGroup 時要自己處理支持 wrap_content 效果。
  • 直接繼承 View 的控件菇肃,需要在 draw 方法中處理 padding地粪。直接繼承 ViewGroup 的控件需要在 onMeasure 和 onLayout 中考慮 padding、子元素 margin 的影響琐谤。
  • 盡量不要在 View 中使用 Handler蟆技,因為 View 本身提供 post 系列方法。
  • View 中如果有線程或者動畫需要停止斗忌,考慮在 onDetachedFromWindow 中處理质礼。該方法當包含此 View 的 Activity 退出或者 View 被 remove 時會被調(diào)用。
  • View 帶有嵌套滑動情形時织阳,需要處理好滑動沖突眶蕉。
自定義 View 示例

直接繼承 View 的示例,主要 3 個點:

  • 支持 padding:onDraw 里面獲取 padding唧躲,并進行處理造挽。
  • 支持 wrap_content:onMeasure 里面判斷 AT_MOST 模式,使用需要的大小
  • 支持自定義屬性:寫一個自定義屬性集合的 xml 文件惊窖,在構(gòu)造方法中加載刽宪、解析即可。

以下是實現(xiàn)上述 3 點界酒,作用為畫一個圓形的自定義 View 代碼:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <!--自定義屬性結(jié)合名-->
  <declare-styleable name="CircleView">
    <!--屬性id 和值的格式 還可以是 string/integer/boolean 等-->
    <attr name="circle_color" format="color"/>
  </declare-styleable>
</resources>
public class CircleView extends View {

  private int mColor = Color.RED;
  private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

  public CircleView(Context context) {
    super(context);
    init();
  }

  public CircleView(Context context, @Nullable AttributeSet attrs) {
    //使用自定義屬性時 應(yīng)該為調(diào)用另一個構(gòu)造方法
    this(context, attrs, 0);
  }

  public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //加載自定義屬性集合 CircleView
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
    //解析 CircleView 屬性集合 中 id 為 circleView_circle_color 的屬性
    mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
    a.recycle();
    init();
  }

  private void init() {
    mPaint.setColor(mColor);
  }

  //支持padding
  @Override protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int pL = getPaddingLeft();
    int pR = getPaddingRight();
    int pT = getPaddingTop();
    int pB = getPaddingBottom();
    int w = getWidth() - pL - pR;
    int h = getHeight() - pT - pB;
    int radius = Math.min(w, h) / 2;
    //把圓心定在左和上方除去padding后的位置加上設(shè)定寬高的一半的位置
    canvas.drawCircle(w / 2 + pL, h / 2 + pT, radius, mPaint);
  }

  //支持 wrap_content 即當寬高設(shè)為 wrap_content 時默認為 200 pt
  @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(200, 200);
    } else if (wSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(200, hSpecSize);
    } else if (hSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(wSpecSize, 200);
    }
  }
}

直接繼承 ViewGroup 示例:
這種方法實現(xiàn)比較復(fù)雜圣拄,以下是一個帶有水平滑動效果的類似 LinearLayout(水平方向上)的自定義View,基本思路是 onMeasure 里毁欣,當判斷模式為 wrap_content 時庇谆,獲取子 View 的寬高岳掐,進行相應(yīng)處理,onLayout 中饭耳,使用一個變量保存寬度累加值串述,實現(xiàn)水平排列。另外此例實現(xiàn)了內(nèi)容滑動效果寞肖,且解決了滑動沖突纲酗。

public class HorizontalScrollViewEx extends ViewGroup {

  private int mChildrenSize;  //子View數(shù)量
  private int mChildWidth;    //子 View 寬度 本例默認子View大小是一樣的
  private int mChildIndex;    //處于屏幕可見最左邊的子 View 序號

  private int mLastX;
  private int mLastY;
  private int mLastXInterceptTouchEvent;
  private int mLastYInterceptTouchEvent;

  private Scroller mScroller;
  private VelocityTracker mVelocityTracker;

  public HorizontalScrollViewEx(Context context) {
    super(context);
    init();
  }

  public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
  }

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

  //實例化 Scroller 和 VelocityTracker(速度追蹤)
  private void init() {
    if (mScroller == null) {
      mScroller = new Scroller(getContext());
      mVelocityTracker = VelocityTracker.obtain();
    }
  }

  //包含此 View 的 Activity 退出或者 View 被 remove 時會被調(diào)用 回收資源
  @Override protected void onDetachedFromWindow() {
    mVelocityTracker.recycle();
    super.onDetachedFromWindow();
  }

  @Override public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();

    switch (ev.getAction()) {
      case MotionEvent.ACTION_DOWN:
        intercepted = false;
        if (!mScroller.isFinished()) {
          //滑動動畫效果未完成時 再次傳來事件序列 則中止滑動動畫 且攔截事件
          mScroller.abortAnimation();
          intercepted = true;
        }
        break;
      case MotionEvent.ACTION_MOVE:
        int deltaX = x - mLastXInterceptTouchEvent;
        int deltaY = y - mLastYInterceptTouchEvent;
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
          intercepted = true;
        } else {
          //如果 Y 軸滑動距離大于 X 軸,不攔截事件
          intercepted = false;
        }
        break;
      case MotionEvent.ACTION_UP:
        intercepted = false;
        break;
      default:
        break;
    }
    mLastX = x;
    mLastY = y;
    mLastXInterceptTouchEvent = x;
    mLastYInterceptTouchEvent = y;

    return intercepted;
  }

  //自身的滑動事件
  @Override public boolean onTouchEvent(MotionEvent event) {
    mVelocityTracker.addMovement(event);
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        if (!mScroller.isFinished()) {
          mScroller.abortAnimation();
        }
        break;
      case MotionEvent.ACTION_MOVE:
        int deltaX = x - mLastX;
        int deltaY = y = mLastY;
        scrollBy(-deltaX, 0);
        break;
      case MotionEvent.ACTION_UP:
        //獲取當前View邊緣與View內(nèi)容左邊緣的距離
        int scrollX = getScrollX();
        mVelocityTracker.computeCurrentVelocity(1000);
        float xVelocity = mVelocityTracker.getXVelocity();
        //mChildIndex代表在屏幕最左邊的子View的序號 0開始
        if (Math.abs(xVelocity) > 50) {
          //如果是快速滑動 判斷滑動方向 使最左邊一個子View的序號加減1
          mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
        } else {
          //當前View邊緣與View內(nèi)容左邊緣的距離加上半個子 View 的寬度 除去子View寬新蟆,使滑動超過半個子View寬情況下
          // 滑動到下個子View 否則只滑動整數(shù)個子View寬度
          mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
        }
        //使mChildIndex不會超過實際子View數(shù)觅赊,以及防止負數(shù)出現(xiàn)
        mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
        //在smoothScrollBy調(diào)用前 實際狀態(tài)是滑動距離是 正scrollX 所以減去
        // 使得手指離開后一直處于滑動整數(shù)個子View的狀態(tài)
        int dx = mChildIndex * mChildWidth - scrollX;
        smoothScrollBy(dx, 0);
        mVelocityTracker.clear();
        break;
      default:
        break;
    }
    mLastX = x;
    mLastY = y;
    return true;
  }

  //緩慢滑動 需要 computeScroll 配合
  private void smoothScrollBy(int dx, int dy) {
    mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
    invalidate();
  }

  @Override public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
      postInvalidate();
    }
  }

  @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measuredWidth = 0;
    int measuredHeight = 0;
    final int childCount = getChildCount();
    //遍歷子View 進行measure
    measureChildren(widthMeasureSpec, heightMeasureSpec);

    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

    //若無子元素,設(shè)置寬高為0
    if (childCount == 0) {
      setMeasuredDimension(0, 0);
    } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
      //若寬高都使用了 wrap content
      final View childView = getChildAt(0);
      //使用第一個子View的寬乘以數(shù)量  其他需求可以考慮改寫 measureChildren 等方法
      measuredWidth = childView.getMeasuredWidth() * childCount;
      //使用第一個子View的高
      measuredHeight = childView.getMeasuredHeight();
      setMeasuredDimension(measuredWidth, measuredHeight);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
      //若高度使用了 wrap content
      final View childView = getChildAt(0);
      //使用第一個子View的高度
      measuredHeight = childView.getMeasuredHeight();
      setMeasuredDimension(widthSpecSize, measuredHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
      //若寬度使用了 wrap content
      final View childView = getChildAt(0);
      //使用第一個子View的寬乘以數(shù)量  其他需求可以考慮改寫 measureChildren 等方法
      measuredWidth = childView.getMeasuredWidth() * childCount;
      setMeasuredDimension(measuredWidth, heightSpecSize);
    }
  }

  @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childLeft = 0;
    final int childCount = getChildCount();
    mChildrenSize = childCount;

    for (int i = 0; i < childCount; i++) {
      final View childView = getChildAt(i);
      if (childView.getVisibility() != View.GONE) {
        final int childWidth = childView.getMeasuredWidth();
        mChildWidth = childWidth;
        childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
        //累加所有子View的寬度
        childLeft += childWidth;
      }
    }
  }
}

以上代碼存在兩個不規(guī)范之處琼稻,一是吮螺,沒有子元素時不應(yīng)該直接設(shè)寬高為 0。應(yīng)該根據(jù) LayoutParams 進行處理帕翻。二是鸠补,在measure 和 layout 中都沒有根據(jù)自身的 padding 和子 View 的 margin 進行處理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嘀掸,一起剝皮案震驚了整個濱河市紫岩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌横殴,老刑警劉巖被因,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異衫仑,居然都是意外死亡,警方通過查閱死者的電腦和手機堕花,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門文狱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缘挽,你說我怎么就攤上這事瞄崇。” “怎么了壕曼?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵苏研,是天一觀的道長。 經(jīng)常有香客問我腮郊,道長摹蘑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任轧飞,我火速辦了婚禮衅鹿,結(jié)果婚禮上撒踪,老公的妹妹穿的比我還像新娘。我一直安慰自己大渤,他們只是感情好制妄,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著泵三,像睡著了一般耕捞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上烫幕,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天砸脊,我揣著相機與錄音,去河邊找鬼纬霞。 笑死凌埂,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的诗芜。 我是一名探鬼主播瞳抓,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼伏恐!你這毒婦竟也來了孩哑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤翠桦,失蹤者是張志新(化名)和其女友劉穎横蜒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡绘证,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年签餐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澎蛛。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蜕窿,靈堂內(nèi)的尸體忽然破棺而出谋逻,到底是詐尸還是另有隱情,我是刑警寧澤桐经,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布毁兆,位于F島的核電站,受9級特大地震影響阴挣,放射性物質(zhì)發(fā)生泄漏气堕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望送巡。 院中可真熱鬧摹菠,春花似錦、人聲如沸骗爆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摘投。三九已至煮寡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間犀呼,已是汗流浹背幸撕。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留外臂,地道東北人坐儿。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像宋光,于是被迫代替她去往敵國和親貌矿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容