溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Android怎么自定義View

發(fā)布時間:2022-05-27 09:08:47 來源:億速云 閱讀:149 作者:zzz 欄目:開發(fā)技術(shù)

這篇文章主要介紹“Android怎么自定義View”,在日常操作中,相信很多人在Android怎么自定義View問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Android怎么自定義View”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

1、為什么需要自定義View

Android系統(tǒng)內(nèi)置的View不滿足我們的業(yè)務(wù)需求

2、自定義View的基本方法

  • onMeasure:決定著View的大小

  • onLayout:決定View在ViewGroup中的位置

  • onDraw:決定繪制什么樣的View

通常情況下:

  • 自定義View只需要重寫onMeasure和onDraw這兩個方法

  • 自定義ViewGroup只需要重寫onMeasure和onLayout這兩個方法

3、自定義View的屬性如何操作

在values文件中創(chuàng)建attr文件,然后使用< declare-styleable >為自定義View添加屬性,在xml中設(shè)置相應(yīng)的屬性值,然后再自定義View的構(gòu)造方法中獲取屬性值(AtrributeSet),將獲取到的屬性值應(yīng)用到View中去

4、View的視圖結(jié)構(gòu)

  • 1、每一個Activity都有一個Window,Window用于顯示我們的界面,Activity負(fù)責(zé)管理Window

  • 2、每個Window都有一個根View->DecorView,Window本身不能顯示界面,需要依托于View

  • 3、DecorView是一個FrameLayout,它主要由兩部分組成,一部分是ActionBar,一部分是一個id為android.R.content的FrameLayout,我們寫好的Activity的根部局的View就是被添加到這里去了,通過setContentView()方法

  • 4、在往下就是一個樹形結(jié)構(gòu)的視圖結(jié)構(gòu),ViewGroup中嵌套ViewGroup和View

FrameLayout rootView = findViewById(android.R.id.content); 
RelativeLayout relativeLayout = (LinearLayout) rootView.getChildAt(0);//獲取Activity的根部局

注意:無論是measure過程還是layout過程還是draw過程,永遠(yuǎn)都是從View樹的根節(jié)點(diǎn)往下樹形遞歸的開始測量或者計算。

5、View的坐標(biāo)系

Android怎么自定義View

注意:

1、當(dāng)view沒有發(fā)生動畫偏移的時候,getX()和getLeft()相等,如果由translation的時候,getX() = getLeft() + getTranslationX()

2、getLeft()等獲取的值是相對父容器而言的

6、View樹的繪制流程

View樹的繪制是交給ViewRootImpl去負(fù)責(zé)的,入口在 ViewRootImpl.setView() --> requestLayout()方法中進(jìn)行的,最終調(diào)用到了一個叫做performTraversals()方法里面,這里面就開始了真正的繪制流程工作,平時寫的onDraw、onMeasure、onLayout也都在這里邊。

6.1 measure過程

1、系統(tǒng)為什么需要measure過程

因?yàn)槲覀冊趯懖季值臅r候要針對不同的機(jī)型做適配,不能寫死view的高度和寬度,經(jīng)常使用wrap_content這種形式,為了適配這種自適應(yīng)布局的機(jī)制,所以系統(tǒng)需要進(jìn)行measure測量

2、measure過程做了什么事情

確定每個view在屏幕上顯示的時候所需要的真實(shí)的寬度和高度

3、ViewGroup如何向子View傳遞限制信息

通過MeasureSpec,從名字上來看叫做測量規(guī)格,它封裝了父容器對子View的布局上的限制,內(nèi)部提供了寬高的信息(SpecMode、SpecSize),SpecSize是指在某種情況下SpecMode下的參考尺寸。

6.2 分析自定義ViewGroup的onMeasure過程

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  int width = 0;//最終確定的寬度
  int height = 0;//最終確定的高度
  //1、首先測量自身
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  //2、為每個子view計算測量的限制信息Mode/Size
  int widthMeasureSpecMode = MeasureSpec.getMode(widthMeasureSpec);
  int widthMeasureSpecSize = MeasureSpec.getSize(widthMeasureSpec);
  int heightMeasureSpecMode = MeasureSpec.getMode(heightMeasureSpec);
  int heightMeasureSpecSize = MeasureSpec.getSize(heightMeasureSpec);
  //3、測量子View;把上一步確定的限制信息,傳遞給每一個子View,然后子View開始measure自己的尺寸
  int childCount = getChildCount();
  for(int i=0;i<childCount;i++){
    View child = getChildAt(i);
    measureChild(child,widthMeasureSpec,heightMeasureSpec);//這個方法就是確定子view的測量大小
  }
  //4、根據(jù)子View的測量尺寸以及自身的SpecMode計算自己的尺寸
  switch (widthMeasureSpecMode) {
    case MeasureSpec.EXACTLY://如果是確定值,則使用確定值
      width = widthMeasureSpecSize;
    case MeasureSpec.AT_MOST://如果是根據(jù)內(nèi)容定的大小
    case MeasureSpec.UNSPECIFIED://一般可以不用單獨(dú)處理
      for(int i=0;i<childCount;i++){
        View child = getChildAt(i);
        int childWidth = child.getMeasuredWidth();//這一步只有當(dāng)measureChild方法執(zhí)行完之后才能拿到
        width = Math.max(childWidth,width);
      }
    default:break;
  }
  switch (heightMeasureSpecMode) {
    case MeasureSpec.EXACTLY://如果是確定值,則使用確定值
      height = heightMeasureSpecSize;
    case MeasureSpec.AT_MOST://如果是根據(jù)內(nèi)容定的大小
    case MeasureSpec.UNSPECIFIED:
      for(int i=0;i<childCount;i++){
        View child = getChildAt(i);
        int childHeight = child.getMeasuredHeight();//這一步只有當(dāng)measureChild方法執(zhí)行完之后才能拿到
        height+=childHeight;
      }
    default:break;
  }
  //保存自身測量后的寬和高
  setMeasuredDimension(width,height);
}

要明確一點(diǎn),重寫自定義ViewGroup的onMeasure方法是為了確定這個View的真正的寬度和高度,很明顯這與它的子View脫離不了干系。

onMeasure()方法中的兩個參數(shù),是這個自定義ViewGroup的父View給出的參考值,具體怎么給出的呢,可以參考ViewGroup的measureChild()方法,這個方法我們在重寫onMeasure時也用到了,看這個方法的第一個參數(shù)好像是View,看起來好像跟我們自定義ViewGroup沒啥關(guān)系,但別忘了,ViewGroup也是一個View,所以,我們的自定義ViewGroup的onMeasure()方法中的兩個參數(shù)就是由下面的方法產(chǎn)生的,具體來講就是下面的 childWidthMeasureSpec和childHeightMeasureSpec。

總結(jié)一句話就是:子View(包括子ViewGroup)的WidthMeasureSpec和HeightMeasureSpec的確定是由子View本身的LayoutParams以及父View(包括父ViewGroup)的WidthMeasureSpec和HeightMeasureSpec確定的。這一段邏輯是ViewGroup#getChildMeasureSpec()。有個表格

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

知道了自身的MeasureSpec參數(shù),下面就好辦了,那么直接調(diào)用view.measure(childWidthMeasureSpec, childHeightMeasureSpec)完成自身的測量。

關(guān)鍵來了, 在View的measure方法里面會調(diào)用onMeasure方法,如果當(dāng)前View是一個普通的View,則直接執(zhí)行這里的方法,完成普通View的測量過程,但是, 如果當(dāng)前View是一個ViewGroup就會調(diào)用自身重寫好的onMeasure方法,也就是我們重寫的方法。

對于自定義ViewGroup重寫的onMeasure方法需要結(jié)合子View的寬度和高度,以及自身的LayOutParams的模式來確定最終的寬度和高度

那么對于普通View是否就不需要重寫onMeasure了呢,源碼不是已經(jīng)寫好了嗎?

看一下代碼:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

發(fā)現(xiàn),無論是精確模式,還是wrap_content模式最后值都是之前由子View本身的LayoutParams以及父View(包括父ViewGroup)的WidthMeasureSpec和HeightMeasureSpec確定的measureSpecSize值大小,通過查表可知,如果當(dāng)普通的自定義View的寬度或者高度被設(shè)置成了為了wrap_content的話,它的效果跟mathch_parent效果一樣,所以普通的自定義View需要對wrap_content這一情況進(jìn)行完善,參考TextView

6.3 分析自定義ViewGroup的onLayout過程

onLayout的中后四個參數(shù),指的是,當(dāng)前自定義ViewGroup在它的父布局中的上下左右坐標(biāo),通過這個坐標(biāo)可以得到當(dāng)前自定義ViewGroup的測量寬度和高度,不過一般也不需要用到這個四個參數(shù),因?yàn)榭梢灾苯油ㄟ^ getMeasuredWidth() 方法得到

所以onLayout的核心目的就是計算每一個控件的left、top、right、bottom坐標(biāo),然后通過 child.layout()方法set進(jìn)去就行了,所以onLayout主要工作就在于如何確定這四個參數(shù)。

追蹤child.layout()方法進(jìn)去看看:

Android怎么自定義View

6.4 自定義Layout實(shí)戰(zhàn)

流布局:

package com.example.materialdesign.selfView;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.RequiresApi;
public class FlowLayout extends ViewGroup {
  public FlowLayout(Context context) {
    super(context);
  }
  public FlowLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
  }
  public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }
  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
  }
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int lineWidth = 0;//記錄每一行的寬度,最終的寬度是由所有行中的最大值
    int lineHeight = 0;//記錄每一行的高度,取決于每一行中最高的那個組件
    int resH = 0;//最終的高度
    int resW = 0;//最終的寬度
    //1、首先測量自身
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //2、為每個子view計算測量的限制信息Mode/Size
    int widthMeasureSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthMeasureSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMeasureSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightMeasureSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    //3、測量每個子view的寬度和高度
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
      View child = getChildAt(i);
      measureChild(child, widthMeasureSpec, heightMeasureSpec);
      MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
      int childMeasuredWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
      int childMeasuredHeight = child.getMeasuredHeight() + lp.bottomMargin + lp.topMargin;

      if (lineWidth + childMeasuredWidth > widthMeasureSpecSize) {//當(dāng)前行的的寬度已經(jīng)加上當(dāng)前view的寬度已經(jīng)大于建議值寬度了
        //需要換行
        resW = Math.max(resW, lineWidth);
        resH += lineHeight;
        //重新賦值
        lineWidth = childMeasuredWidth;
        lineHeight = childMeasuredHeight;
      } else {//不需要換行則累加
        lineWidth += childMeasuredWidth;
        lineHeight = Math.max(lineHeight,childMeasuredHeight);//取最高的那個
      }
      if (i == childCount - 1) {//別忘了單獨(dú)處理最后一行的最后一個元素的情況
        resH += lineHeight;
        resW = Math.max(resW, lineWidth);
      }
    }
    setMeasuredDimension((widthMeasureSpecMode==MeasureSpec.EXACTLY)?widthMeasureSpecSize:resW,
    (heightMeasureSpecMode==MeasureSpec.EXACTLY)?heightMeasureSpecSize:resH);
  }
  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int count = getChildCount();
    int lineWidth = 0;//累加當(dāng)前行的行寬
    int lineHeight = 0;//累加當(dāng)前行的行高
    int top = 0, left = 0;//當(dāng)前控件的left坐標(biāo)和top坐標(biāo)
    for (int i = 0; i < count; i++) {
      View child = getChildAt(i);
      MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
      int childMeasuredWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
      int childMeasuredHeight = child.getMeasuredHeight() + lp.bottomMargin + lp.topMargin;
      //根據(jù)是否要換行,來計算當(dāng)前控件的top坐標(biāo)和Left坐標(biāo),是否換行是需要考慮margin的
      if (childMeasuredWidth + lineWidth > getMeasuredWidth()) {
        top += lineHeight;
        left = 0;
        lineHeight = childMeasuredHeight;
        lineWidth = childMeasuredWidth;
      } else {
        lineWidth += childMeasuredWidth;
        lineHeight = Math.max(lineHeight, childMeasuredHeight);
      }
      //在已知left和top情況下計算當(dāng)前View的上下左右坐標(biāo),在真正給當(dāng)前View定位置時候需要考慮margin的
      int lc = left + lp.leftMargin;
      int tc = top + lp.topMargin;
      int rc = lc + child.getMeasuredWidth();//注意在layout的時候沒有算上margin
      int bc = tc + child.getMeasuredHeight();
      child.layout(lc, tc, rc, bc);
      left += childMeasuredWidth;//下一起點(diǎn)算上margin
    }
  }
  @Override
  protected LayoutParams generateLayoutParams(LayoutParams p) {
    return new MarginLayoutParams(p);
  }
  @Override
  public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(),attrs);
  }
  @Override
  protected LayoutParams generateDefaultLayoutParams() {
    return new MarginLayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
  }
}

注意:上述代碼實(shí)際上可能不符合業(yè)務(wù)預(yù)期,在于 measureChild(child, widthMeasureSpec, heightMeasureSpec);這一句,我們直接調(diào)用系統(tǒng)的方法去獲得子View的MeasureSpec,但實(shí)際上獲取到的值不一定是我們想要的,即下圖的值不一定符合我們的業(yè)務(wù),所以在真正測量子View的時候,需要針對子View的match_parent情況或者wrap_content情況進(jìn)行特殊處理

Android怎么自定義View

一般情況下是針對子View是match_parent的情況做處理,比如我們自定義的FlowLayout,如果FlowLayout是match_parent、子View是match_parent的話,就需要特殊處理了,根據(jù)模式表子View所占的空間將充滿整個父View的剩余空間,這一點(diǎn)符合代碼邏輯但是可能不會符合業(yè)務(wù)需求 

Android怎么自定義View

6.5 細(xì)節(jié)

1、getMeasuredWidth和getWidth的區(qū)別

getMeasuredWidth是在measure的過程結(jié)束后就可以獲得到的View測量寬度值;而getWidth是在layout過程結(jié)束后通過mRight-mLeft得到的;一般情況下,二者是相等的,但有可能不相等,getWidth取決于layout過程中怎么算的四點(diǎn)坐標(biāo)值。

2、onDraw、onMeasure以及onLayout會多次調(diào)用,所以這里面盡量不要頻繁的new 對象

3、調(diào)用view.invalidate()以及requestLayout()有什么區(qū)別:

這個方法是用來刷新整個視圖的,當(dāng)視圖的內(nèi)容,可見性發(fā)生變化,onDraw(Canvas canvas)方法會被調(diào)用。 調(diào)用invalidate()方法不會導(dǎo)致measure和layout方法被調(diào)用。

requestLayout()是在view的布局發(fā)生變化時調(diào)用,布局的變化包含位置,大小。重新觸發(fā)measure,layout,draw

注意:

  • 1.這個方法不能在正在布局的時候調(diào)用

  • 2.調(diào)用這個方法,會導(dǎo)致布局重繪,調(diào)用measure,layout,draw的過程。

到此,關(guān)于“Android怎么自定義View”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI