Android 自定義View基礎(一)

View的分類

類別 解釋 特色
單一視圖 即一個View,如TextView 不包含子View
視圖組 即多個View組成的ViewGroup,如LinearLayout 包含子View

建立徹底自定義的組件

建立自定義的組件主要圍繞着如下五個方面:java

  • 繪圖(Drawing): 控制視圖的渲染,一般經過覆寫onDraw方法來實現
  • 交互(Interaction): 控制用戶和視圖的交互方式,好比OnTouchEvent,gestures
  • 尺寸(Measurement): 控制視圖內容的維度,經過覆寫onMeasure方法
  • 屬性(Attributes): 在XML中定義視圖的屬性,使用TypedArray來獲取屬性值
  • 持久化(Persistence): 配置發生改變時保存和恢復狀態,經過onSaveInstanceState和onRestoreInstanceState

因爲Android的2D渲染如今能夠比較好的支持硬件加速了,可是在自定義控件進行繪製是仍是有不少api不兼容的,因此在自定義控件的時候,在你不能100%確認你使用的api支持硬件加速的話,最好把硬件加速關閉了,不然有可能出現一些莫名其妙的問題:android

  1. 硬件加速關閉方法

在清單文件的application節點下進行關閉或者打開,這種方式是做用於整個應用的:canvas

<!--false表示關閉,true表示打開-->
 android:hardwareAccelerated="false"
複製代碼
  1. 在activity註冊時進行關閉或者打開,這種方式只做用於該activity:
<activity
   android:name=".WebViewTest"
  android:hardwareAccelerated="false"/>
複製代碼

三、在指定View初始化時關閉或者打開,這種方式只做用於該View控件:api

//若是是自定義的view,可在構造方法中調用該方法,便可開啓或者關閉硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
複製代碼

View類簡介

  • View類是Android中各類組件的基類,如View是ViewGroup基類
  • View表現爲顯示在屏幕上的各類視圖

View的構造函數

共有4個,具體以下:bash

// 若是View是在Java代碼裏面new的,則調用第一個構造函數
    public CustomView(Context context) {
        super(context);
    }

    // 若是View是在.xml裏聲明的,則調用第二個構造函數
    // 自定義屬性是從AttributeSet參數傳進來的
    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // 不會自動調用
    // 通常是在第二個構造函數裏主動調用
    // 如View有style屬性時
   public CustomView(Context context,  AttributeSet attrs,
                      int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    //API21以後才使用
    // 不會自動調用
    // 通常是在第二個構造函數裏主動調用
    // 如View有style屬性時
    @TargetApi(21)
    public CustomView(Context context,  AttributeSet attrs,
                      int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
複製代碼

添加視圖到佈局中

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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">

    <!-- 添加自定義View -->
    <com.zeroxuan.customviewtest.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
        
</android.support.constraint.ConstraintLayout>
複製代碼

自定義屬性

視圖能夠經過XML來配置屬性和樣式,你須要想清楚要添加那些自定義的屬性,好比咱們想讓用戶能夠選擇形狀的顏色、是否顯示形狀的名稱,好比咱們想讓視圖能夠像下面同樣配置:app

<com.zeroxuan.customviewtest.CustomView
        android:layout_width="wrap_content"
        app:shapeColor="#FF0000"
        app:displayShapeName="true"
        android:layout_height="wrap_content" 
        ···/>
複製代碼

爲了可以定義shapeColor和displayShapeName,咱們須要在res/values/中新建一個文件名爲custom_view_attrs.xml的文件(文件名隨意),在這個文件中包含<resources></resources>標籤,添加<declare-styleable name="ShapeSelectorView"></declare-styleable>標籤,標籤的name屬性一般是自定義的類名,在declare-styleable中添加attr元素,attr元素是key (「name=」) -- value (「format=」)的形式:ide

<?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屬性是咱們指望的值的類型,好比color,dimension,boolean,integer,float等。一旦定義好了屬性,你能夠像使用自帶屬性同樣使用他們,惟一的區別在於你的自定義屬性屬於一個不一樣的命名空間,你能夠在根視圖的layout裏面定義命名空間,通常狀況下你只須要這樣指定:http://schemas.android.com/apk/res/<package_name>,可是你可使用http://schemas.android.com/apk/res-auto自動解析命名空間。函數

應用自定義屬性

  • 方式一
public class CustomView extends View {
    private int shapeColor;
    private boolean displayShapeName;

    public CustomView(Context context) {
        super(context);
        initCustomView(null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initCustomView(attrs);

    }

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

    @TargetApi(21)
    public CustomView(Context context, AttributeSet attrs,
                      int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initCustomView(attrs);
    }

    private void initCustomView(AttributeSet attrs) {

        if (attrs == null) {
            return;
        }

        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CustomView);

        try {
            shapeColor = a.getColor(R.styleable.CustomView_shapeColor, Color.WHITE);
            displayShapeName = a.getBoolean(R.styleable.CustomView_displayShapeName, false);
        } finally {
            a.recycle();
        }
    }
}
複製代碼
  • 方式二
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 defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);

    }

    @TargetApi(21)
    public CustomView(Context context, AttributeSet attrs,
                      int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        if (attrs == null) {
            return;
        }

        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CustomView);

        try {
            shapeColor = a.getColor(R.styleable.CustomView_shapeColor, Color.WHITE);
            displayShapeName = a.getBoolean(R.styleable.CustomView_displayShapeName, false);
        } finally {
            a.recycle();
        }
    }
}
複製代碼

建議使用方式一,好比你自定義的View繼承自ListView或者TextView的時候,ListView或者TextView內部的構造函數會有一個默認的defStyle, 第二種方法調用時defStyle會傳入0,這將覆蓋基類中默認的defStyle,進而致使一系列問題。佈局

接下來添加一些getter和setter方法post

public class ShapeSelectorView extends View {
  // ...
  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();
  }
}
複製代碼

當視圖屬性發生改變的時候可能須要從新繪圖,你須要調用`invalidate()`和`requestLayout()`來刷新顯示。

Android 繪製順序

draw()

draw() 是繪製過程的總調度方法。一個 View 的整個繪製過程都發生在 draw() 方法裏。背景、主體、子 View 、滑動相關以及前景的繪製,它們其實都是在 draw() 方法裏的。

// View.java 的 draw() 方法的簡化版大體結構(是大體結構,不是源碼哦):

public void draw(Canvas canvas) {
    ...

    drawBackground(Canvas); // 繪製背景(不能重寫)
    onDraw(Canvas); // 繪製主體
    dispatchDraw(Canvas); // 繪製子 View
    onDrawForeground(Canvas); // 繪製滑動相關和前景

    ...
}
複製代碼

從上面的代碼能夠看出,onDraw() dispatchDraw() onDrawForeground() 這三個方法在 draw() 中被依次調用,所以它們的遮蓋關係就是——dispatchDraw() 繪製的內容蓋住 onDraw() 繪製的內容;onDrawForeground() 繪製的內容蓋住 dispatchDraw() 繪製的內容。而在它們的外部,則是由 draw() 這個方法做爲總的調度。因此,你也能夠重寫 draw() 方法來作自定義的繪製。

view_draw

想在滑動邊緣漸變、滑動條和前景之間插入繪製代碼?雖然這三部分是依次繪製的,但它們被一塊兒寫進了 onDrawForeground()方法裏,因此你要麼把繪製內容插在它們以前,要麼把繪製內容插在它們以後。而想往它們之間插入繪製,是作不到的。

寫在 super.draw() 的下面

因爲 draw() 是總調度方法,因此若是把繪製代碼寫在 super.draw() 的下面,那麼這段代碼會在其餘全部繪製完成以後再執行,也就是說,它的繪製內容會蓋住其餘的全部繪製內容。

它的效果和重寫 onDrawForeground(),並把繪製代碼寫在 super.onDrawForeground() 的下面效果是同樣的:都會蓋住其餘的全部內容。

固然了,雖然說它們效果同樣,但若是你既重寫 draw() 又重寫 onDrawForeground(),那麼 draw() 裏的內容仍是會蓋住 onDrawForeground() 裏的內容的。因此嚴格來說,它們的效果仍是有一點點不同的。

寫在 super.draw() 的上面

因爲 draw() 是總調度方法,因此若是把繪製代碼寫在 super.draw() 的上面,那麼這段代碼會在其餘全部繪製以前被執行,因此這部分繪製內容會被其餘全部的內容蓋住,包括背景。

例如:

EditText重寫它的 draw() 方法,而後在 super.draw() 的上方插入代碼,以此來在全部內容的底部塗上一片綠色:

public AppEditText extends EditText {
    ...

    public void draw(Canvas canvas) {
        canvas.drawColor(Color.parseColor("#F0FF0000")); // 塗上紅色

        super.draw(canvas);
    }
}
複製代碼

view_draw_red

注意:出於效率的考慮,ViewGroup默認會繞過 draw() 方法,換而直接執行 dispatchDraw(),以此來簡化繪製流程。因此若是你自定義了某個 ViewGroup 的子類而且須要在它的除 dispatchDraw() 之外的任何一個繪製方法內繪製內容,你可能會須要調用 View.setWillNotDraw(false) 這行代碼來切換到完整的繪製流程

Android座標系

其中棕色部分爲手機屏幕

View座標系

View的座標系統是相對於父控件而言的

  1. 原始位置(不受偏移量影響,單位是像素px)
/* 獲取子View左上角距父View頂部的距離 * 即左上角縱座標 */
getTop();       


/* 獲取子View左上角距父View左側的距離 * 即左上角橫座標 */
getLeft();   

/* 獲取子View右下角距父View頂部的距離 * 即右下角縱座標 */
getBottom();    

/* 獲取子View右下角距父View左側的距離 * 即右下角橫座標 */
getRight();     
複製代碼
  1. 寬高和座標的關係
width = right - left;
height = bottom - top;
複製代碼
  1. Android 新增的參數

    1. x,y:View的左上角座標
    2. translationX,translationY:相對於父容器的偏移量(有get/set方法)。

    注意:View在平移過程當中,原始位置不會改變。

    // 換算關係
    x = left + translationX
    y = top + translationY
    複製代碼
    1. 從API21開始增長了z(垂直屏幕方向)和elevation(浮起來的高度,3D)
  2. dp與px(像素)相互轉換代碼

// dp轉爲px
public static int dp2px(Context context, float dpValue) {
    final float scale = context.getResources().getDisplayMetrics().density;
    return (int) (dpValue * scale + 0.5f);
}

// px轉爲dp
public static int px2dp(Context context, float pxValue) {
    final float scale = context.getResources().getDisplayMetrics().density;
    return (int) (pxValue / scale + 0.5f);
}
複製代碼

MotionEvent

  • 手指觸摸屏幕後產生的事件,典型事件以下:
ACTION_DOWN–手指剛觸摸屏幕
ACTION_MOVE–手指在屏幕上移動
ACTION_UP–手指從屏幕上分開的一瞬間
複製代碼
  • MotionEvent獲取點擊事件發生的座標
getX (相對於當前View左上角的座標)
getY
getRawX(相對於屏幕左上角的座標)
getRawY
複製代碼
  • TouchSlop滑動最小距離

    • 滑動小於這個常量,系統將不會認爲這是滑動(常量爲8dp,使用時系統會自動轉爲px)
    • 獲取方式
    ViewConfiguration.get(getContext()).getScaledTouchSlop();
    複製代碼
  • 示例

float x = 0, y = 0;
  @Override
  public boolean onTouchEvent(MotionEvent event) {
// 獲取TouchSlop(滑動最小距離)
      float slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
      switch (event.getAction()) {
          case MotionEvent.ACTION_DOWN:
              Log.e(TAG, "onTouchEvent: " + "按下");
              Log.e(TAG, "getX: " + event.getX());
              Log.e(TAG, "getY: " + event.getY());
              Log.e(TAG, "getRawX: " + event.getRawX());
              Log.e(TAG, "getRawY: " + event.getRawY());

              x = event.getX();
              y = event.getY();

              break;
          case MotionEvent.ACTION_MOVE:
              Log.e(TAG, "onTouchEvent: " + "移動");
              break;
          case MotionEvent.ACTION_UP:
              Log.e(TAG, "onTouchEvent: " + "鬆開" + x);
              if (event.getX() - x > slop) {
                  Log.e(TAG, "onTouchEvent: " + "往右滑動" + event.getX());
              } else if (x - event.getX() > slop) {
                  Log.e(TAG, "onTouchEvent: " + "往左滑動" + event.getX());
              } else {
                  Log.e(TAG, "onTouchEvent: " + "無效滑動" + event.getX());
              }
              x = 0;
              y = 0;
              break;
      }
      // 返回true,攔截這個事件
      // 返回false,不攔截
      return true;
  }
複製代碼

GestureDetector

  • 輔助檢測用戶的單擊、滑動、長按、雙擊等行爲
  • 使用
    • 建立一個GestureDetector對象並實現OnGestureListener接口,根據須要實現OnDoubleTapListener接口
    // 解決長按屏幕後沒法拖動的現象,可是這樣會沒法識別長按事件
    mGestureDetector.setIsLongpressEnable(false);
    複製代碼
  • 接管目標View的onTouchEvent方法
    return mGestureDetector.onTouchEvent(event);
    複製代碼
  • 示例
private GestureDetector mGestureDetector;
... ...
private void init(Context context){
       this.mContext = context;
       mGestureDetector = new GestureDetector(mContext,onGestureListener);
       mGestureDetector.setOnDoubleTapListener(onDoubleTapListener);
       //解決長按屏幕沒法拖動,可是會形成沒法識別長按事件
	//mGestureDetector.setIsLongpressEnabled(false);
   }
	
   @Override
   public boolean onTouchEvent(MotionEvent event) {
       // 接管onTouchEvent
       return mGestureDetector.onTouchEvent(event);
   }
	
   GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
       @Override
       public boolean onDown(MotionEvent e) {
           Log.i(TAG, "onDown: 按下");
           return true;
       }
	
       @Override
       public void onShowPress(MotionEvent e) {
           Log.i(TAG, "onShowPress: 剛碰上還沒鬆開");
       }
	
       @Override
       public boolean onSingleTapUp(MotionEvent e) {
           Log.i(TAG, "onSingleTapUp: 輕輕一碰後立刻鬆開");
           return true;
       }
	
       @Override
       public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
           Log.i(TAG, "onScroll: 按下後拖動");
           return true;
       }
	
       @Override
       public void onLongPress(MotionEvent e) {
           Log.i(TAG, "onLongPress: 長按屏幕");
       }
	
       @Override
       public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
           Log.i(TAG, "onFling: 滑動後鬆開");
           return true;
       }
   };
	
   GestureDetector.OnDoubleTapListener onDoubleTapListener = new GestureDetector.OnDoubleTapListener() {
       @Override
       public boolean onSingleTapConfirmed(MotionEvent e) {
           Log.i(TAG, "onSingleTapConfirmed: 嚴格的單擊");
           return true;
       }
	
       @Override
       public boolean onDoubleTap(MotionEvent e) {
           Log.i(TAG, "onDoubleTap: 雙擊");
           return true;
       }
	
       @Override
       public boolean onDoubleTapEvent(MotionEvent e) {
           Log.i(TAG, "onDoubleTapEvent: 表示發生雙擊行爲");
           return true;
       }
   };
複製代碼

目錄結構

相關文章
相關標籤/搜索