一篇文章搞懂Android 自定義viewgroup的難點

2021-03-02 Android 編程之旅

聲明 本文轉載自:DK_BurNIng
原文連結:https://juejin.im/post/5b25bc136fb9a00e373bd0c8
已獲得作者授權,如需轉載請與作者聯繫

本文的目的

目的在於教會大家到底如何自定義viewgroup,自定義布局和自定義測量到底如何寫。很多網上隨便搜搜的概念和流程圖 這裡不再過多描述了,建議大家看本文之前,先看看基本的自定義viewgroup流程,心中有個大概即可。本文注重於實踐

viewgroup 的測量布局流程基本梳理

稍微回顧下,基本的viewgroup繪製和布局流程中的重點:

1.view 在onMeasure()方法中進行自我測量和保存,也就是說對於view(不是viewgroup噢)來說一定在onMeasure方法中 計算出自己的尺寸並且保存下來

2.viewgroup實際上最終也是循環從上大小來調用子view的measure方法,注意子view的measure其實最終調用的是子view的onMeasure 方法。所以我們理解這個過程為: viewgroup循環遍歷調用所有子view的onmeasure方法,利用onmeasure方法計算出來的大小,來確定這些子view最終可以佔用的大小和所處的布局的位置。

3.measure方法是一個final方法,可以理解為做測量工作準備工作的,既然是final方法所以我們無法重寫它,不需要過多 關注他,因為measure最終要調用onmeasure ,這個onmeasure我們是可以重寫的。要關注這個。layout和onlayout是一樣的 關係。

4.父view調用子view的layout方法的時候會把之前measure階段確定的位置和大小都傳遞給子view。

5.對於自定義view/viewgroup來說 我們幾乎只需要關注下面三種需求:

對於已有的android自帶的view,我們只需要重寫他的onMeasure方法即可。修改一下這個尺寸即可完成需求。

對於android系統沒有的,屬於我們自定義的view,比上面那個要複雜一點,要完全重寫onMeasure方法。

第三種最複雜,需要重寫onmeasure和onlayout2個方法,來完成一個複雜viewgroup的測量和布局。

onMeasure方法的特殊說明:

如何理解父view對子view的限制?

onMeasure的兩個參數既然是父view對子view的限制,那麼這個限制的值到底是哪來的呢?

實際上,父view對子view的限制絕大多數就來自於我們開發者所設置的layout開頭的這些屬性

比方說我們給一個imageview設置了他的layout_width和layout_height 這2個屬性,那這2個屬性其實就是我們開發者 所期望的寬高屬性,但是要注意了, 設置的這2個屬性是給父view看的,實際上對於絕大多數的layout開頭的屬性這些屬性都是設置給父view看的

為什麼要給父view看?因為父view要知道這些屬性以後才知道要對子view的測量加以什麼限制?

到底是不限制(UNSPECIFIED)?還是限制個最大值(AT_MOST),讓子view不超過這個值?還是直接限制死,我讓你是多少就得是多少(EXACTLY)。

自定義一個BannerImageView 修改onMeasure方法

所謂bannerImageview,就是很多電商其實都會放廣告圖,這個廣告圖的寬高比都是可變的,我們在日常開發過程中 也會經常接觸到這種需求:imageview的寬高比 在高保真中都標註出來,但是考慮到很多手機的屏幕寬度或者高度都不確定 所以我們通常都要手動來計算出這個imageview高度或者寬度,然後動態改變width或者height的值。這種方法可用但是很麻煩 這裡給出一個自定義的imageview,通過設置一個ratio的屬性即可動態的設置iv的高度。很是方便

看下效果

最後看下代碼,重要的部分都寫在注釋裡了,不再過多講了。

public class BannerImageView extends ImageView {

   

   public BannerImageView(Context context) {
       super(context);
   }

   public BannerImageView(Context context, AttributeSet attrs) {
       super(context, attrs);

       TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BannerImageView);
       ratio = typedArray.getFloat(R.styleable.BannerImageView_ratio, 1.0f);
       typedArray.recycle();
   }

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

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       
       
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);

       
       int mWidth = getMeasuredWidth();

       int mHeight = (int) (mWidth * ratio);

       
   }
}

自定義view,完全自己寫onMeasure方法

首先明確一個結論:

對於完全自定義的view,完全自己寫的onMeasure方法來說,你保存的寬高必須要符合父view的限制,否則會發生bug, 保存父view對子view的限制的方法也很簡單直接調用resolveSize方法即可。

所以對於完全自定義的view onMeasure方法也不難寫了,

先算自己想要的寬高,比如你畫了個圓,那麼寬高就肯定是半徑的兩倍大小, 要是圓下面還有字, 那麼高度肯定除了半徑的兩倍還要有字體的大小。對吧。很簡單。這個純看你自定義view是啥樣的

算完自己想要的寬高以後 直接拿resolveSize 方法處理一下 即可。

最後setMeasuredDimension 保存。

範例:

public class LoadingView extends View {

   
   int radius;

   
   int left = 10, top = 30;


   Paint mPaint = new Paint();

   public LoadingView(Context context) {
       super(context);
   }

   public LoadingView(Context context, AttributeSet attrs) {
       super(context, attrs);
       TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
       radius = typedArray.getInt(R.styleable.LoadingView_radius, 0);
   }

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

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);


       int width = left + radius * 2;
       int height = top + radius * 2;

       
       
       width = resolveSize(width, widthMeasureSpec);
       height = resolveSize(height, heightMeasureSpec);        setMeasuredDimension(width, height);
   }

   @Override
   protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);

       RectF oval = new RectF(left, top,
               left + radius * 2, top + radius * 2);
       mPaint.setColor(Color.BLUE);
       canvas.drawRect(oval, mPaint);
       
       mPaint.setColor(Color.RED);
       mPaint.setStyle(Paint.Style.STROKE);
       mPaint.setStrokeWidth(2);
       canvas.drawArc(oval, -90, 360, false, mPaint);
   }
}

布局文件:

<LinearLayout
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:background="#000000"
       android:orientation="horizontal">

       <com.example.a16040657.customviewtest.LoadingView
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:src="@mipmap/dly"
           app:radius="200"></com.example.a16040657.customviewtest.LoadingView>

       <com.example.a16040657.customviewtest.LoadingView
           android:layout_marginLeft="10dp"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:src="@mipmap/dly"
           app:radius="200"></com.example.a16040657.customviewtest.LoadingView>
   </LinearLayout>

最後效果:

自定義一個viewgroup

這個其實也就是稍微複雜了一點,但是還是有跡可循的,只是稍微需要一點額外的耐心。

自定義一個viewgroup 需要注意的點如下:

一定是先重寫onMeasure確定子view的寬高和自己的寬高以後 才可以繼續寫onlayout 對這些子view進行布局噢~~

viewgroup 的onMeasure其實就是遍歷自己的view 對自己的每一個子view進行measure,絕大多數時候對子view的 measure都可以直接用 measureChild()這個方法來替代,簡化我們的寫法,如果你的viewgroup很複雜的話 無法就是自己寫一遍measureChild 而不是調用measureChild 罷了。

計算出viewgroup自己的尺寸並且保存,保存的方法還是哪個setMeasuredDimension 不要忘記了

逼不得已要重寫measureChild方法的時候,其實也不難無非就是對父view的測量和子view的測量 做一個取捨關係而已, 你看懂了基礎的measureChild方法,以後就肯定會寫自己的複雜的measureChild方法了。

下面是一個極簡的例子,一個很簡單的flowlayout的實現,沒有對margin paddding做處理,也假設了每一個tag的高度 是固定的,可以說是極為簡單了,但是麻雀雖小 五臟俱全,足夠你們好好理解自定義viewgroup的關鍵點了。


public class SimpleFlowLayout extends ViewGroup {
   public SimpleFlowLayout(Context context) {
       super(context);
   }

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

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

   

   @Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
       int childTop = 0;
       int childLeft = 0;
       int childRight = 0;
       int childBottom = 0;

       
       int usedWidth = 0;


       
       int layoutWidth = getMeasuredWidth();
       Log.v("wuyue", "layoutWidth==" + layoutWidth);        for (int i = 0; i < getChildCount(); i++) {
           View childView = getChildAt(i);
           
           int childWidth = childView.getMeasuredWidth();
           int childHeight = childView.getMeasuredHeight();

           
               childLeft = 0;
               usedWidth = 0;
               childTop += childHeight;
               childRight = childWidth;
               childBottom = childTop + childHeight;
               childView.layout(0, childTop, childRight, childBottom);
               usedWidth = usedWidth + childWidth;
               childLeft = childWidth;                continue;
           }
           childRight = childLeft + childWidth;
           childBottom = childTop + childHeight;
           childView.layout(childLeft, childTop, childRight, childBottom);
           childLeft = childLeft + childWidth;
           usedWidth = usedWidth + childWidth;

       }
   }

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

       
       
       int widthMode = MeasureSpec.getMode(widthMeasureSpec);
       int heightMode = MeasureSpec.getMode(heightMeasureSpec);
       int widthSize = MeasureSpec.getSize(widthMeasureSpec);
       int heightSize = MeasureSpec.getSize(heightMeasureSpec);


       int usedWidth = 0;      
       int remaining = 0;      
       int totalHeight = 0;    
       int lineHeight = 0;    
           View childView = getChildAt(i);
           LayoutParams lp = childView.getLayoutParams();

           
           measureChild(childView, widthMeasureSpec, heightMeasureSpec);
           
           remaining = widthSize - usedWidth;

           
               
               usedWidth = 0;
               
               totalHeight = totalHeight + lineHeight;
           }

           
           usedWidth = usedWidth + childView.getMeasuredWidth();
           
           lineHeight = childView.getMeasuredHeight();
       }

       
           heightSize = totalHeight;
       }        setMeasuredDimension(widthSize, heightSize);
   }
}

最後看下效果

推薦閱讀:

Android 常用設計模式之——單例模式

個人收藏視頻資源 | 一大波乾貨來襲

android應用開發者,你們真的了解Activity的生命周期嗎?

Android開發需要掌握的設計模式——工廠模式

輕鬆學習Java設計模式之責任鏈模式

人人都能組件化

溫馨提示:

我創建了一個技術交流群,群裡有各個行業的大佬都有,大家可以在群裡暢聊技術方面內容,以及文章推薦;如果有想加入的夥伴加我微信號【luotaosc】備註一下「加群

另外關注公眾號,還有一些個人收藏的視頻:

回復「學習資源」 ,獲取學習視頻。

原創文章不易,如果覺得寫得好,掃碼關注一下點個讚,是我最大的動力。

備註:程序圈LT

相關焦點

  • Android自定義View入門及實戰案例分析
    2017年安卓巴士全球開發者論壇-成都站【線下沙龍第六站】終於要去全世界最好的成都和你們見面了前言  上次我們講了一篇文章徹底搞懂複寫view中的一些函數3. 為自定義View類增加屬性(兩種方式)4. 繪製控制項(導入布局)5. 響應用戶事件6. 定義回調函數(根據自己需求來選擇)二、哪些方法需要被重寫   view中onDraw()是個空函數,也就是說具體的視圖都要覆寫該函數來實現自己的繪製。
  • android 自定義view大小 - CSDN
    --場景1-->android:layout_width="match_parent"android:layout_height="match_parent"那麼按照我們的期望,希望子View的尺寸要是300dp*300dp,如果子View的布局參數是<!
  • Android 自定義View篇(十)實現跑馬燈垂直滾動效果
    本文是對上篇文章的一個補充,股票 APP 列表底部有一個實時更新交易的跑馬燈效果,縱觀市面上很多產品都應用到這個效果,決定自己動手實現一下。開發準備工作 1、實現效果圖1、自定義 ViewFlipper 屬性設置以下屬性,建議使用自定義屬性方式,便於後期修改和 XML 中使用。
  • android 自定義view大小專題及常見問題 - CSDN
    Android自定義View概述Android開發進階的必經之路為什麼要自定義View自定義View的基本方法自定義View的最基本的三個方法分別是: onMeasure()、onLayout()、onDraw();View在Activity中顯示出來,要經歷測量、布局和繪製三個步驟,分別對應三個動作:measure、layout和draw。
  • Android 自定義View 點讚效果
    因此有了此文,如果文中有錯還望各位小夥伴指出出來,自定義View的大佬可以跳過了,(*^__^*) 嘻嘻……我們還是先看看實現效果:接下來我們看看實現方法。分析:><android.support.constraint.ConstraintLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:agreeview="http://schemas.android.com/apk/res-auto"    xmlns:tools
  • Android RecyclerView自定義LayoutManager
    在第一篇中已經講過,LayoutManager主要用於布局其中的Item,在LayoutManager中能夠對每個Item的大小,位置進行更改,將它放在我們想要的位置,在很多優秀的效果中,都是通過自定義LayoutManager來實現的,比如:Github: https://github.com
  • 這些都是Android中不規則形狀View的布局實現!
    而對於非方形的,Android官方並沒有給出非常好的解決方案.有的無非就是自定義View了.然而自定義View非常麻煩,需要重寫很多方法,而且稍微不注意可能就會喪失一些特性或者造成一些Bug。而且即便是自定義View,其實那個自定義View還是方的!!!
  • Android-SetContentView內部原理
    ><TextView xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns
  • 一個Android資深工程師的自我提升
    看到一篇文章中提到「最近幾年國內的初級Android程式設計師已經很多了,但是中高級的Android技術人才仍然稀缺「,這的確不假,從我在百度所進行的一些面試來看,找一個適合的高級Android工程師的確不容易,一般需要進行大量的面試才能挑選出一個比較滿意的。為什麼中高級Android程式設計師不多呢?
  • Android實現快遞時間軸功能
    2.實現思路使用RecyclerView,自定義RecyclerView.ItemDecoration複習ItemDecoration中getItemOffsets()方法,重寫onDraw<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/
  • 從零開始的Android新項目8 - Data Binding高級篇
    原文:http://blog.zhaiyifan.cn/2016/07/06/android-new-project-from-0-p8/本文是MarkZhai同學系列文章的第8篇,剛剛完稿,此文承接 《從零開始的Android新項目7 - Data Binding入門篇》,繼續介紹Data Binding的進階內容,建議沒看過上篇的同學先前往閱讀,效果更佳,第7篇早前並沒有在我公眾號發布
  • Android如何獲取WebView內容高度
    ><com.trs.studyview.view.TRSScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView"
  • 架構組件之 ViewModel | 中文教學視頻
    如果您需要自定義 ViewModel 的構造函數,則使用 ViewModelProvider.NewInstanceFactory。我們還推薦開發者同時使用 ViewModel 和另一個生命周期組件 LiveData 來實現響應式的 UI 界面。
  • Android設置選項開發及自定義Preference樣式
    4 android:title=設置 >567 android:title=關於 >89101112 preference:tipstring=>13preference:titlestring=自定義測試 >1415 android:action=android.intent.action.VIEW16 android:data=http://www.baidu.com />171819
  • Android平臺View的按鍵事件KeyDown用法
    摘要:平時設計自己的顯示類View時需要捕獲按鍵事件,比如KeyEvent、首先引入android.view.KeyEvent
  • Android實現導航欄添加消息數目提示功能
    寫一篇短小精悍,好用的知識積累吧。開發中時常會出現信息提醒,新內容提示等等一堆問題。其實就是在各種控制項或者是item上面加「小圓點」。
  • Android N功能:自定義快捷圖標/應用更簡潔
    Android N功能:自定義快捷圖標/應用更簡潔  據外媒androidheadlines報導稱,谷歌將為Android 7.0正式版增加一個新特性,即允許用戶自定義快速設置中心  外媒稱,此次自定義快速設置欄並不是普通意義上的自定義,而是向三方開發者開放了更多的通知欄快捷圖標權限,這意味著任何開發者都可以在快速設置中開發一些小功能,並向裡面增加新項目,甚至可以無縫集成。
  • Android自定義View-蜘蛛網屬性圖(五邊形圖)
    參考的文章:Android自定義控制項 芝麻信用分雷達圖這裡為了尊重上面這篇文章的作者,需要說明一下,下面的代碼有部分是參考上面這篇文章的。這裡我學習之後有了自己的理解。做了一點小改動,然後以自己的思路來捋一捋。希望我的文字對你更有幫助,哈哈。
  • Google 開源的 Android 排版庫:FlexboxLayout
    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent"    app
  • Android子線程也能修改UI?
    這個時候我的腦子也是一篇懵逼的。如果是onCreate開了子線程,然後子線程立刻更新UI,那是不會出現閃退的。具體原因這篇文章(https://www.jianshu.com/p/1b2ccd3e1f1f)有詳細解釋過。但是沉睡5秒鐘還是能修改成功,這就讓我有點吃驚了。