1. 程式人生 > >安卓進階之自定義View

安卓進階之自定義View

pro andro mes tco 反饋 resources 裁切 schema parent

目錄

  • 安卓進階之自定義View
    • 自定義View的工作流程和內容
      • 工作流程
      • 測量階段和布局階段的工作內容
      • View 和 ViewGroup 在測量階段和布局階段的區別
      • 繪制階段的工作內容
    • 上手:實現繼承View的自定義View
    • 上手:自定義ViewGroup
    • 補充: 繪制內容的關鍵點

安卓進階之自定義View

自定義View,可以分為具體的三大類:

  • 自定義View(繼承系統控件/繼承View)
  • 自定義Viewgroup(繼承系統特定的Viewgroup/繼承ViewGround)
  • 自定義組合控件

自定義View的工作流程和內容

工作流程

無論是哪一類View,只要是View,都需要經過以下工作流程:

測量measure->布局layout->繪制draw.

  1. measure階段測量View的寬高
  2. layout階段用來確定View的位置
  3. draw階段則是用來繪制View.

測量階段和布局階段的工作內容

測量階段(measure):從上到下遞歸地調用每個 View 或者 ViewGroup 的 measure() 方法,測量他們的尺寸並計算它們的位置;

布局階段(layout):從上到下遞歸地調用每個 View 或者 ViewGroup 的 layout() 方法,把測得的它們的尺寸和位置賦值給它們。

View 和 ViewGroup 在測量階段和布局階段的區別

  1. 測量階段,measure() 方法被父 View 調用,在 measure() 中做一些準備和優化工作後,調用 onMeasure() 來進行實際的自我測量。 onMeasure() 做的事,ViewViewGroup 不一樣:
    1. ViewViewonMeasure() 中會計算出自己的尺寸然後保存;
    2. ViewGroupViewGrouponMeasure() 中會調用所有子 View 的 measure() 讓它們進行自我測量,並根據子 View 計算出的期望尺寸來計算出它們的實際尺寸和位置(實際上 99.99% 的父 View 都會使用子 View 給出的期望尺寸來作為實際尺寸,原因在下期或下下期會講到)然後保存。同時,它也會根據子 View 的尺寸和位置來計算出自己的尺寸然後保存;
  2. 布局階段,layout() 方法被父 View 調用,在 layout() 中它會保存父 View 傳進來的自己的位置和尺寸,並且調用 onLayout() 來進行實際的內部布局。onLayout() 做的事, ViewViewGroup 也不一樣:
    1. View:由於沒有子 View,所以 ViewonLayout() 什麽也不做。
    2. ViewGroup:ViewGroup 在 onLayout() 中會調用自己的所有子 View 的 layout() 方法,把它們的尺寸和位置傳給它們,讓它們完成自我的內部布局。

繪制階段的工作內容

在官方註釋中,繪制分為以下步驟:

  1. 如果需要,就繪制背景--drawBackgrounp()
  2. 保存當前canvas層
  3. 繪制View的內容--onDraw()
  4. 繪制子View--dispatchView()
  5. 如果需要,就繪制View的褪色邊緣,類似於陰影效果
  6. 繪制裝飾,比如滾動條--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工作流程中的測量階段,布局階段對應方法進行重寫:

  1. 重寫onMesure()
  2. 重寫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