View的分類
類別 | 解釋 | 特點 |
---|---|---|
單一檢視 | 即一個View,如TextView | 不包含子View |
檢視組 | 即多個View組成的ViewGroup,如LinearLayout | 包含子View |
建立完全自定義的元件
建立自定義的元件主要圍繞著以下五個方面:
- 繪圖(Drawing): 控制檢視的渲染,通常通過覆寫onDraw方法來實現
- 互動(Interaction): 控制使用者和檢視的互動方式,比如OnTouchEvent,gestures
- 尺寸(Measurement): 控制檢視內容的維度,通過覆寫onMeasure方法
- 屬性(Attributes): 在XML中定義檢視的屬性,使用TypedArray來獲取屬性值
- 持久化(Persistence): 配置發生改變時儲存和恢復狀態,通過onSaveInstanceState和onRestoreInstanceState
View類簡介
- View類是Android中各種元件的基類,如View是ViewGroup基類
- View表現為顯示在螢幕上的各種檢視
View的建構函式
共有4個,具體如下:
// 如果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來配置屬性和樣式,你需要想清楚要新增那些自定義的屬性,比如我們想讓使用者可以選擇形狀的顏色、是否顯示形狀的名稱,比如我們想讓檢視可以像下面一樣配置:
<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=”)
的形式:
<?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方法
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() 方法來做自定義的繪製。
想在滑動邊緣漸變、滑動條和前景之間插入繪製程式碼?雖然這三部分是依次繪製的,但它們被一起寫進了
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);
}
}
複製程式碼
注意:出於效率的考慮,
ViewGroup
預設會繞過draw()
方法,換而直接執行dispatchDraw()
,以此來簡化繪製流程。所以如果你自定義了某個 ViewGroup 的子類並且需要在它的除dispatchDraw()
以外的任何一個繪製方法內繪製內容,你可能會需要呼叫View.setWillNotDraw(false)
這行程式碼來切換到完整的繪製流程。
Android座標系
其中棕色部分為手機螢幕
View座標系
View的座標系統是相對於父控制元件而言的
- 原始位置(不受偏移量影響,單位是畫素px)
/* 獲取子View左上角距父View頂部的距離
* 即左上角縱座標
*/
getTop();
/* 獲取子View左上角距父View左側的距離
* 即左上角橫座標
*/
getLeft();
/* 獲取子View右下角距父View頂部的距離
* 即右下角縱座標
*/
getBottom();
/* 獲取子View右下角距父View左側的距離
* 即右下角橫座標
*/
getRight();
複製程式碼
- 寬高和座標的關係
width = right - left;
height = bottom - top;
複製程式碼
-
Android 新增的引數
x
,y
:View的左上角座標- translationX,translationY:相對於父容器的偏移量(有get/set方法)。
注意:View在平移過程中,原始位置不會改變。
// 換算關係 x = left + translationX y = top + translationY 複製程式碼
- 從API21開始增加了z(垂直螢幕方向)和elevation(浮起來的高度,3D)
-
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;
}
};
複製程式碼