安卓進階之自定義View
目錄
- 安卓進階之自定義View
- 自定義View的工作流程和內容
- 工作流程
- 測量階段和布局階段的工作內容
- View 和 ViewGroup 在測量階段和布局階段的區別
- 繪制階段的工作內容
- 上手:實現繼承View的自定義View
- 上手:自定義ViewGroup
- 補充: 繪制內容的關鍵點
- 自定義View的工作流程和內容
安卓進階之自定義View
自定義View,可以分為具體的三大類:
- 自定義View(繼承系統控件/繼承View)
- 自定義Viewgroup(繼承系統特定的Viewgroup/繼承ViewGround)
- 自定義組合控件
自定義View的工作流程和內容
工作流程
無論是哪一類View,只要是View,都需要經過以下工作流程:
測量measure->布局layout->繪制draw.
- measure階段測量View的寬高
- layout階段用來確定View的位置
- draw階段則是用來繪制View.
測量階段和布局階段的工作內容
測量階段(measure):從上到下遞歸地調用每個 View 或者 ViewGroup 的 measure() 方法,測量他們的尺寸並計算它們的位置;
布局階段(layout):從上到下遞歸地調用每個 View 或者 ViewGroup 的 layout() 方法,把測得的它們的尺寸和位置賦值給它們。
View 和 ViewGroup 在測量階段和布局階段的區別
- 測量階段,
measure()
方法被父 View 調用,在measure()
中做一些準備和優化工作後,調用onMeasure()
來進行實際的自我測量。onMeasure()
做的事,View
和ViewGroup
不一樣:- View:
View
在onMeasure()
中會計算出自己的尺寸然後保存; - ViewGroup:
ViewGroup
在onMeasure()
中會調用所有子 View 的measure()
讓它們進行自我測量,並根據子 View 計算出的期望尺寸來計算出它們的實際尺寸和位置(實際上 99.99% 的父 View 都會使用子 View 給出的期望尺寸來作為實際尺寸,原因在下期或下下期會講到)然後保存。同時,它也會根據子 View 的尺寸和位置來計算出自己的尺寸然後保存;
- View:
- 布局階段,
layout()
方法被父 View 調用,在layout()
中它會保存父 View 傳進來的自己的位置和尺寸,並且調用onLayout()
來進行實際的內部布局。onLayout()
做的事,View
和ViewGroup
也不一樣:- View:由於沒有子 View,所以
View
的onLayout()
什麽也不做。 - ViewGroup:ViewGroup 在 onLayout() 中會調用自己的所有子 View 的 layout() 方法,把它們的尺寸和位置傳給它們,讓它們完成自我的內部布局。
- View:由於沒有子 View,所以
繪制階段的工作內容
在官方註釋中,繪制分為以下步驟:
- 如果需要,就繪制背景--drawBackgrounp()
- 保存當前canvas層
- 繪制View的內容--onDraw()
- 繪制子View--dispatchView()
- 如果需要,就繪制View的褪色邊緣,類似於陰影效果
- 繪制裝飾,比如滾動條--onDrawForeground()
其中第2,5步可以跳過.具體如何繪制內容將在文末補充.
上手:實現繼承View的自定義View
我們通過繼承View實現一個自定義View,往往需要實現以下內容:
繪制內容(draw)
對Padding進行處理(draw)
對wrap_content進行處理(measure)
創建自定義屬性,配置自己的自定義View
重寫onTounchEvent()改變觸摸反饋
括號為涉及到的工作流程.
例:
在界面中,創建一個可以滑動的矩形
java代碼:
public class CustomView extends View { Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); int lastx; int lasty; int mColor; @Override public boolean onTouchEvent(MotionEvent event) { int x= (int) event.getX(); int y= (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastx=x; lasty=y; break; case MotionEvent.ACTION_MOVE: int offsetX=x-lastx; int offsetY=y-lasty; ((View)getParent()).scrollBy(-offsetX,-offsetY); break; case MotionEvent.ACTION_UP: break; } return true; } public CustomView(Context context) { super(context); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); //提取CustomView屬性集合的rect_colot屬性,如果不設置,默認為紅色. TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.CustomView); mColor=typedArray.getColor(R.styleable.CustomView_rect_color, Color.RED); typedArray.recycle(); paint.setColor(mColor); paint.setStrokeWidth((float)1.5); } public CustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } /* widthMeasureSpec和heightMeasureSpec分別壓縮了mode和size兩個信息. mode的分類: UNSPECIFIED:不限制,相當於match_parent AT_MOST:限制上限,相當於wrap_content EXACTLY:限制固定值 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //重新 onMeasure(),並計算出 View 的尺寸; //(可以使用 resolveSize() 來讓子 View 的計算結果符合父 View 的限制,也可以用自己的方式來滿足父 View 的限制也行), 本例子使用自己方式滿足父 View 的限制。 //對wrap_content進行處理 //在onMeasure方法中指定一個默認的寬高,在設置wrap_content屬性時設置此默認寬高 int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec); int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec); int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec); int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec); if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(80,80); }else if(widthSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(80,heightSpecSize); }else if(heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,80); } /*resolveSize(int size, int widthMeasureSpec) 方法內部的實現方式與例子自定義實現方式相似,從widthMeasureSpec得到 mode的類別作判斷 UNSPECIFIED:不限制,返回size AT_MOST:限制上限,size>MeasureSpec.getSize(widthMeasureSpec),返回後者,否則返回直接size EXACTLY:限制固定值,返回MeasureSpec.getSize(widthMeasureSpec) */ /*setMeasuredDimension( resolveSize(int size,int widthMeasureSpec), resolveSize(int size,int heightMeasureSpec) ); */ } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //對padding進行處理 int paddingLeft=getPaddingLeft(); int paddingRight=getPaddingRight(); int paddingTop=getPaddingTop(); int paddingBottom=getPaddingBottom(); int width=getWidth()-paddingLeft-paddingRight; int height=getHeight()-paddingTop-paddingBottom; //根據padding繪制矩形 canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,paint); } }
xml文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.example.drawtest.CustomView app:rect_color="@color/colorAccent" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
以android開頭的都是系統自帶的屬性,自定義屬性需要在values目錄下創建attrs.xml:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CustomView"> <attr name="rect_color" format="color"/> </declare-styleable> </resources>
上手:自定義ViewGroup
自定義ViewGroup的步驟其實就是對View工作流程中的測量階段,布局階段對應方法進行重寫:
- 重寫onMesure()
- 重寫onLayout()
重寫 onMeasure() 的步驟:
1.遍歷子View,根據子View的LayoutParams屬性以及ViewGroup的MeasureSpec模式得到子 View 的 MeasureSpec
(子View的LayoutParams就是開發者對子View寬高等與位置相關屬性的要求,而ViewGroup的mode則是開發者對ViewGroup寬高屬性的要求.更簡單的說,子View的LayoutParams保存了子View的xml代碼中的Layout_height,Layout_width等有關的位置信息屬性,ViewGroup的mode保存了ViewGroup的xml代碼中的Layout_height,Layout_width屬性).
2.把計算出的子View的childWidthSpec和childHeightSpec作為參數傳入子 View 的 measure()方法 來計算子 View 的尺寸
3.子View在onMeasure()中計算自己最終的位置和尺寸利用setMeasuredDimension()方法保存
4.ViewGroup通過子View的位置和尺寸確定自己的尺寸並用 setMeasuredDimension() 保存
重寫 onLayout() 的方式
在 onLayout() 裏調用每個子 View 的 layout() ,讓它們保存自己的位置和尺寸。
補充: 繪制內容的關鍵點
自定義繪制的方式最常用的方式是重寫onDraw()繪制方法
繪制的關鍵是 Canvas 的使用
- Canvas 的繪制類方法: drawXXX() (關鍵參數:Paint)
- Canvas 的輔助類方法:範圍裁切和幾何變換
Paint:
Paint
的 API 大致可以分為 4 類:顏色,效果,drawText() 相關,初始化.顏色類的API作用包括:直接設置顏色的 API 用來給圖形和文字設置顏色(純色,漸變色);加濾鏡; 用來處理源圖像和 View 已有內容的關系。
效果類的 API 可以實現抗鋸齒、填充/輪廓、線條寬度、線頭形狀,線拐角,線性過濾(使圖像過渡平緩)等等。
drawText()與初始化使用較少不作介紹.
Canvas範圍裁剪和幾何變換:
範圍裁剪可以得到對原圖像進行裁剪得到各種形狀的圖像,比如輸入方形圖片,輸出圓形頭像.
幾何變換可以實現平移,旋轉,縮放,錯切效果;
可以使用不同的繪制方法來控制遮蓋關系
參考學習網站:HenCoderhttps://hencoder.com/
參考學習書籍:Android進階之光-劉望舒
安卓進階之自定義View