類別 | 解釋 | 特色 |
---|---|---|
單一視圖 | 即一個View,如TextView | 不包含子View |
視圖組 | 即多個View組成的ViewGroup,如LinearLayout | 包含子View |
建立自定義的組件主要圍繞着如下五個方面:java
因爲Android的2D渲染如今能夠比較好的支持硬件加速了,可是在自定義控件進行繪製是仍是有不少api不兼容的,因此在自定義控件的時候,在你不能100%確認你使用的api支持硬件加速的話,最好把硬件加速關閉了,不然有可能出現一些莫名其妙的問題:android
- 硬件加速關閉方法
在清單文件的application節點下進行關閉或者打開,這種方式是做用於整個應用的:canvas
<!--false表示關閉,true表示打開--> android:hardwareAccelerated="false" 複製代碼
- 在activity註冊時進行關閉或者打開,這種方式只做用於該activity:
<activity android:name=".WebViewTest" android:hardwareAccelerated="false"/> 複製代碼
三、在指定View初始化時關閉或者打開,這種方式只做用於該View控件:api
//若是是自定義的view,可在構造方法中調用該方法,便可開啓或者關閉硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null); 複製代碼
共有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()`來刷新顯示。
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()
方法裏,因此你要麼把繪製內容插在它們以前,要麼把繪製內容插在它們以後。而想往它們之間插入繪製,是作不到的。
因爲 draw()
是總調度方法,因此若是把繪製代碼寫在 super.draw()
的下面,那麼這段代碼會在其餘全部繪製完成以後再執行,也就是說,它的繪製內容會蓋住其餘的全部繪製內容。
它的效果和重寫 onDrawForeground()
,並把繪製代碼寫在 super.onDrawForeground()
的下面效果是同樣的:都會蓋住其餘的全部內容。
固然了,雖然說它們效果同樣,但若是你既重寫
draw()
又重寫onDrawForeground()
,那麼draw()
裏的內容仍是會蓋住onDrawForeground()
裏的內容的。因此嚴格來說,它們的效果仍是有一點點不同的。
因爲 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)
這行代碼來切換到完整的繪製流程。
其中棕色部分爲手機屏幕
View的座標系統是相對於父控件而言的
/* 獲取子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);
}
複製代碼
ACTION_DOWN–手指剛觸摸屏幕
ACTION_MOVE–手指在屏幕上移動
ACTION_UP–手指從屏幕上分開的一瞬間
複製代碼
getX (相對於當前View左上角的座標)
getY
getRawX(相對於屏幕左上角的座標)
getRawY
複製代碼
TouchSlop滑動最小距離
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;
}
複製代碼
// 解決長按屏幕後沒法拖動的現象,可是這樣會沒法識別長按事件
mGestureDetector.setIsLongpressEnable(false);
複製代碼
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;
}
};
複製代碼