android 自定義View過程解析

原文地址:http://www.mamicode.com/info-detail-506763.html


總所周知,安卓UI是基於View(屏幕上的單一節點)和ViewGroup(屏幕上節點的集合),在android中有很多widgets和layouts可以用於創建UI界面,比如最常見的View有Button,TextView等等,而最常見的佈局也有RelativeLayout,LinearLayout等。

在一些應用中我們不得不自定義View去滿足我們的需求,自定義View可以繼承一個View或者已存在的子類去創建我們自己的自定義View,甚至可以用SurfaceView去做更復雜的繪圖。

創建一個自定義View的一般步驟是繼承View或者其子類,重寫一些方法比如onDraw,onMeasure,onLayout,onTouchEvent,然後再activity中使用我們的自定義View。

我們主要是通過以下五個方面創建一個自定義View 
1,繪圖,通過重寫onDraw方法控制View在屏幕上的渲染效果 
2,交互,通過重寫onTouchEvent方法或者使用手勢來控制用戶的交互 
3,測量,通過重寫onMeasure方法來對控件進行測量 
4,屬性,可以通過xml自定義控件的屬性,然後通過TypedArray來進行使用 
5,狀態的保存,爲了避免配置改變時丟失View狀態,通過重寫onSaveInstanceState,onRestoreInstanceState方法來保存和恢復狀態

可能這樣說比較籠統,我們通過一個例子來進一步瞭解,假設我們需要一個View允許用戶選擇不同的形狀,而這個控件只會顯示一些簡單的形狀,比如正方形,圓形,三角形,通過點擊圖形能夠在不同形狀之間切換。先看下效果圖,不斷點擊進行切換。 
技術分享

一、定義自定義View的類。 
爲了創建點擊可切換的形狀的自定義View,我們繼承View,編寫構造方法。實現三個構造方法,最終調用三個參數的構造方法。

public class CustomView extends View {

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
}

二、把自定義View加入到Layout中。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <cn.edu.zafu.view.CustomView
        android:id="@+id/customview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        />

</RelativeLayout>

三、定義自定義屬性。 
一個良好的自定義控件應該是能通過xml進行控制的,所以我們需要考慮一下我們的自定義View的哪些屬性需要被提取到xml中,比如,我們應該可以讓用戶選擇圖形的顏色,是否顯示圖形的名稱等。我們可以通過下面的代碼在xml中進行配置

  <cn.edu.zafu.view.CustomView
        xmlns:app="http://schemas.android.com/apk/res/cn.edu.zafu.view"
        app:displayShapeName="true"
        app:shapeColor="#7f0000" />

爲了能夠使用圖形的顏色和圖形顯示的名字的屬性,我們應該新建res/values/attrs.xml文件,在裏面定義這些屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="CustomView">
       <attr name="shapeColor" format="color" />
       <attr name="displayShapeName" format="boolean" />
   </declare-styleable>
</resources>

注意上述代碼,我們爲每一個attr節點都寫了name屬性和format屬性,format是屬性的數據結構,合法的值包括string, color, dimension, boolean, integer, float, enum等

一旦我們定義了自定義屬性,我們就可以在xml文件裏進行使用,唯一的區別就是我們自定義屬性的命名空間是不同的,我們需要在佈局的根節點上或者自定義View上定義命名空間,然後才能使用自定義屬性。這裏我直接在View上定義命名空間,完全可以把命名空間提取到根佈局上。

四、應用自定義屬性。 
現在我們已經通過xml設定了自定義屬性shapeColor和displayShapeName,我們需要在構造方法中提取到這些屬性。爲了提取屬性,我們使用TypedArray類和obtainStyledAttributes方法。

public class CustomView extends View {
    private int shapeColor;
    private boolean displayShapeName;

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setupAttributes(attrs);
    }
    private void setupAttributes(AttributeSet attrs) {
        // 提取自定義屬性到TypedArray對象中
        TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs,
                R.styleable.CustomView, 0, 0);
        // 將屬性賦值給成員變量
        try {
            shapeColor = a.getColor(R.styleable.CustomView_shapeColor,
                    Color.BLACK);
            displayShapeName = a.getBoolean(
                    R.styleable.CustomView_displayShapeName, false);
        } finally {
            // TypedArray對象是共享的必須被重複利用。
            a.recycle();
        }
    }
}

五、增加屬性的getter和setter方法

public boolean isDisplayingShapeName() {
    return displayShapeName;
  }

  public void setDisplayingShapeName(boolean state) {
    this.displayShapeName = state;
    invalidate();//重繪
    requestLayout();
  }

  public int getShapeColor() {
    return shapeColor;
  }

  public void setShapeColor(int color) {
    this.shapeColor = color;
    invalidate();
    requestLayout();
  }

注意以上代碼,當View的屬性發生改變時我們需要進行重繪和重新佈局,爲了保證正常進行,請確保調用了invalidate和requestLayout方法。

六、繪製圖形 
接下來,讓我們開始真正使用自定義屬性(顏色,是否顯示圖形名)進行圖形的繪製。所有的View的繪製發生在onDraw方法裏,我們使用其參數Canvas將圖形繪製到View上,現在我們繪製一個正方形。

public class CustomView extends View {

    private int shapeWidth = 100;
    private int shapeHeight = 100;
    private int textXOffset = 0;
    private int textYOffset = 30;
    private Paint paintShape;


    private int currentShapeIndex = 0;

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setupAttributes(attrs);
        setupPaint();
    }
    private void setupPaint() {
        paintShape = new Paint();
        paintShape.setStyle(Style.FILL);
        paintShape.setColor(shapeColor);
        paintShape.setTextSize(30);
    }
}

以上代碼會繪製我們定義的顏色的圖形,如果顯示圖形名,其圖形名也會被顯示,效果圖就跟上面的gif圖片裏的正方形一樣。

七、計算尺寸 
爲了按照用戶定義的寬度高度進行繪製,我們需要重寫onMeasure方法進行View的測量,該方法決定了View的寬度和高度。我們定義的View的寬度和高度由我們的形狀和形狀名字共同決定。我們先看下onMeasure的代碼。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 簡單定義文本邊距
        int textPadding = 10;
        int contentWidth = shapeWidth;
        // 使用測量模式獲得寬度
        int minw = contentWidth + getPaddingLeft() + getPaddingRight();
        int w = resolveSizeAndState(minw, widthMeasureSpec, 0);
        // 同寬度
        int minh = shapeHeight + getPaddingBottom() + getPaddingTop();
        //如果現實圖形名,則加上文字高度
        if (displayShapeName) {
            minh += textYOffset + textPadding;
        }
        int h = resolveSizeAndState(minh, heightMeasureSpec, 0);
        // 測量完成後必須調用setMeasuredDimension方法
        // 之後可以通過getMeasuredWidth 和 getMeasuredHeight 方法取出高度和寬度
        setMeasuredDimension(w, h);
    }

注意以上計算要將View的內邊距計算進去然後再計算整個寬度高度,並且最後必須調用setMeasuredDimension方法設置寬度和高度,resolveSizeAndState() 方法將返回一個合適的尺寸,只要將測量模式和我們計算的寬度高度傳進去即可。該方法在API11開始出現,低於該版本將無法使用該方法,這裏我抽取android的源碼供參考。

/**
     * Utility to reconcile a desired size and state, with constraints imposed
     * by a MeasureSpec.  Will take the desired size, unless a different size
     * is imposed by the constraints.  The returned value is a compound integer,
     * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
     * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting
     * size is smaller than the size the view wants to be.
     *
     * @param size How big the view wants to be
     * @param measureSpec Constraints imposed by the parent
     * @return Size information bit mask as defined by
     * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}.
     */
    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        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:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result | (childMeasuredState&MEASURED_STATE_MASK);
    }

該方法裏設計到了兩處位運算,暫時還沒搞懂這兩處位運算有什麼作用,如果有清除的還請幫忙解釋下作用。

八、在不同圖形之間進行切換 
現在我們已經繪製了正方形,但是我們想讓view在我們點擊它的時候切換圖形,現在我們給它加入事件,我們重寫onTouchEvent方法即可

  private String[] shapeValues = { "square", "circle", "triangle" };
  private int currentShapeIndex = 0;
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    boolean result = super.onTouchEvent(event);
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
      currentShapeIndex ++;
      if (currentShapeIndex > (shapeValues.length - 1)) {
        currentShapeIndex = 0;
      }
      postInvalidate();
      return true;
    }
    return result;
  }

現在無論什麼時候點擊view,都會選中對應的形狀,當postInvalidate 方法被調用後就會進行重繪,現在我們更新onDraw代碼,繪製不同的圖形。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    String shapeSelected = shapeValues[currentShapeIndex];
    if (shapeSelected.equals("square")) {
      canvas.drawRect(0, 0, shapeWidth, shapeHeight, paintShape);
      textXOffset = 0;
    } else if (shapeSelected.equals("circle")) {
      canvas.drawCircle(shapeWidth / 2, shapeHeight / 2, shapeWidth / 2, paintShape);
      textXOffset = 12;
    } else if (shapeSelected.equals("triangle")) {
      canvas.drawPath(getTrianglePath(), paintShape);
      textXOffset = 0;
    }
    if (displayShapeName) {
      canvas.drawText(shapeSelected, 0 + textXOffset, shapeHeight + textYOffset, paintShape);
    }
  }

  protected Path getTrianglePath() {
    Point p1 = new Point(0, shapeHeight), p2 = null, p3 = null;
    p2 = new Point(p1.x + shapeWidth, p1.y);
    p3 = new Point(p1.x + (shapeWidth / 2), p1.y - shapeHeight);
    Path path = new Path();
    path.moveTo(p1.x, p1.y);
    path.lineTo(p2.x, p2.y);
    path.lineTo(p3.x, p3.y);
    return path;
  }

現在我們點擊view,每點擊一次圖形就會進行切換,其效果圖就跟最初貼的gif圖片一樣。

九、完善控件 
增加getter方法獲得圖形名

public String getSelectedShape() {
    return shapeValues[currentShapeIndex];
  }

現在在activity中,我們就可以通過getSelectedShape可以獲取到圖形名了。

十、狀態的保存

當配置改變時,比如手機屏幕發生旋轉,我們必須保存一些數據供從容保證view的狀態不會發生改變。我們通過重寫onSaveInstanceState和onRestoreInstanceState方法來保存和恢復數據。比如,在我們的view中,我們需啊喲保存的數據是當前是什麼圖形,可以通過保存數組的下標currentShapeIndex來實現。

 @Override
  public Parcelable onSaveInstanceState() {
    // 新建一個Bundle
    Bundle bundle = new Bundle();
    // 保存view基本的狀態,調用父類方法即可
    bundle.putParcelable("instanceState", super.onSaveInstanceState());
    // 保存我們自己的數據
    bundle.putInt("currentShapeIndex", this.currentShapeIndex);
    // 當然還可以繼續保存其他數據
    // 返回bundle對象
    return bundle;
  }

  @Override
  public void onRestoreInstanceState(Parcelable state) {
    // 判斷該對象是否是我們保存的
    if (state instanceof Bundle) {
      Bundle bundle = (Bundle) state;
      // 把我們自己的數據恢復
      this.currentShapeIndex = bundle.getInt("currentShapeIndex");
      // 可以繼續恢復之前的其他數據
      // 恢復view的基本狀態
      state = bundle.getParcelable("instanceState");
    }
    // 如果不是我們保存的對象,則直接調用父類的方法進行恢復
    super.onRestoreInstanceState(state);
  }

一旦我們定義這些保存和恢復的方法,我們就能夠在配置發生改變時保存我們必要的數據。

好了,整個流程就大致這樣,可能很多語句都會讀上去不通,但是還是可以湊合看的,整個文章翻譯後自己做過部分整理。希望可以給android剛入門的新手帶來一些幫助,同時呢,大神勿噴。

源碼下載

自定義View過程解析源代碼下載

發佈了20 篇原創文章 · 獲贊 12 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章