標籤:android中 string radius ondraw dem 正是 繼承 textview 事件
View是Android很重要的一部分,常用的View有Button、TextView、EditView、ListView、GridView、各種layout等等,開發人員通過對這些View的各種組合以形成豐富多彩的互動介面,一個應用中介面互動的體驗往往在應用的受歡迎程度上起了很關鍵得作用,所以開發人員們大多會想方設法的做出一個更加精美的介面,例如:通過自訂View、深入學習View的原理以便更好的對其最佳化使其在操作起來更加流暢等等,也正因為如此,在面試中View也常常作為面試官重點考察的對象之一。
View是所有控制項的基類,包括Button、TextView、EditView等等都直接或間接繼承自view,View下面還有ViewGroup子類,即LinearLayout、RelativeLayout等都屬於ViewGroup。
我們需要知道的是在Android中,無論是View還是其他介面,右方向代表著x軸的正向,下方向代表著y軸的正向。
View 的基本工作原理
在 ActivityThread 中,當Activity被建立後會將 DecorView 添加到 Window 中,同時建立 ViewRootImpl 對象,並將 ViewRootImpl 和 DecorView 建立關聯,而 DecorView 就是一個 Activity 的頂級 View,在一個預設的主題中,它分為標題列,和內容地區,我們所添加的 View 均是添加到了 DecorView 的內容地區,這些被添加進去的 View 的工作流程正式通過 ViewRootImpl 完成的。
ViewRoot、DecorView 及 View 的三大流程簡介:
ViewRoot:對應於 ViewRootImpl,連結 WindowManager 和 DecorView 的紐帶,View 的三大流程均是通過它完成的。(View 的繪製流程是從 ViewRoot 的 performTraversals() 方法開始的,它經過 measure、layout、draw 三個流程最終才能將一個 View 完整的繪製出來。)
DecorView:建立一個 Android 應用時我們都知道,預設主題的情況下這個應用的介面會分為兩部分:標題列、內容地區。而這個介面的頂級 View 就是 DecorView。
- View的繪製經過了 measure、layout、draw 三個流程:
- measure:對應 onMeasure() 方法,測量View的寬、高。
- layout:對應 onLayout() 方法,確定view的四個頂點,即確定View在父容器中的位置。
- draw:對應 onDraw(),繪製View。在自訂 View 時我們也正是在 onDraw() 方法內可以在 Canvas 畫布上隨心所欲的畫出我們想要的 View。
自訂 View
自訂 View 的方式不止一種,可以直接繼承 View,重寫 onDraw() 方法,也可以直接繼承 ViewGroup,還可以繼承現有的控制項(如:TextView、LinearLayout)等,本篇主要介紹一下直接繼承 View 的方式。
直接繼承 View 來實現自訂 View 的這種方式比較靈活,可以實現很多複雜的效果,這種方式最關鍵的步驟就是重寫 onDraw() 方法,通過 Paint 畫筆等工具在 Canvas 畫布上進行各種圖案的繪製以達到我們想要的效果。
其實在自訂 View 過程中,痛點往往不是怎麼使用畫筆本身,而是繪製出預期效果的思路,例如:你想通過自訂 View 來做一個折線圖控制項,傳入一組資料怎麼確定這些資料在畫布上對應點的相對座標,而確定點的座標就需要通過相關的數學公式來計算了,推算出合適的公式往往就是解決問題的關鍵。
接下來就用這種方式來寫個圓形的小 demo 來說明一下自訂 View 的流程。
- 建立一個繼承 View 的類,添加構造方法,設定 Paint 畫筆,重寫 onDraw() 方法,先在畫布上以最簡單的方式話一個半徑為100的圓。
/** * 自訂 View 簡單樣本 * Created by liuwei on 17/12/14. */public class MyView extends View { private final static String TAG = MyView.class.getSimpleName(); private Paint mPaint = new Paint(); private int mColor = Color.parseColor("#ff0000"); public MyView(Context context) { super(context); Log.i(TAG, "MyView(Context context):content=" + context); init(); } public MyView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); Log.i(TAG, "MyView(Context context, @Nullable AttributeSet attrs):content=" + context + " | attrs=" + attrs); init(); } private void init() { mPaint.setAntiAlias(true); // 消除鋸齒 mPaint.setColor(mColor); // 為畫筆設定顏色 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 重寫此方法,對自訂控制項在 wrap_content 情況下設定預設寬、高 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heithtSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(200, 200); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(200, heithtSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, 200); } } @Override protected void onDraw(Canvas canvas) { Log.i(TAG, "onDraw: "); super.onDraw(canvas); canvas.drawCircle(100, 100, 100, mPaint); }}
運行結果就是一個紅色的實心圓,在這個樣本中為了使得布局檔案中的 wrap_content 正常生效,重寫了 onMeasure() 方法,關於這個問題,在這篇博文《Android查缺補漏--自訂 View 中 wrap_content 無效的解決方案》中也介紹過了,這裡就不多說了。
- 將上面的圓再擴充一下:做成以畫布的可用性區域域的中心為圓點,畫出最大的圓。同時為自訂 View 設定 padding
對於一個控制項,有 margin 和 padding,margin 是外間距,屬於控制項之外的範圍,在自訂 View 時不需要對 margin 做特殊處理。但 padding 就不同了,是內間距,需要我們在控制項的內部做處理才能讓布局檔案中對控制項設定的 padding 生效。
private int mPaddingTop;private int mPaddingBottom;private int mPaddingLeft;private int mPaddingRight;private int mUsableWidth; // 可用寬度(減去padding後的寬度)private int mUsableHeight;// 可用高度(減去padding後的高度)private int mUsableStartX = 0; // 畫筆起始點的x座標private int mUsableStartY = 0; // 畫筆其實點的y座標private int mCircleX; // 圓心x座標private int mCircleY; // 圓心y座標private int mCircleRadius;// 圓的半徑@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaddingTop = getPaddingTop(); mPaddingBottom = getPaddingBottom(); mPaddingLeft = getPaddingLeft(); mPaddingRight = getPaddingRight(); // 可用寬度和寬度要考慮padding mUsableWidth = getWidth() - mPaddingRight - mPaddingLeft; mUsableHeight = getHeight() - mPaddingTop - mPaddingBottom; // 畫筆起始點要考慮padding mUsableStartX = mPaddingLeft; mUsableStartY = mPaddingTop; // 確定可用性區域域的中心為圓心 mCircleX = mUsableStartX + mUsableWidth / 2; mCircleY = mUsableStartY + mUsableHeight / 2; // 確定圓的半徑,以可用寬度和高度兩者較短的一半為圓的半徑 if (mUsableWidth <= mUsableHeight) { mCircleRadius = mUsableWidth / 2; } else { mCircleRadius = mUsableHeight / 2; } canvas.drawCircle(mCircleX, mCircleY, mCircleRadius, mPaint);}
在布局檔案中設定 paddingLeft 為15dp,paddingRight 為30dp,為了更好看出間距,將控制項的背景顏色設為了黑色,查看效果:
<cn.codingblock.view.reset_view.MyView android:id="@+id/myview" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="10dp" android:paddingLeft="15dp" android:paddingRight="30dp" android:background="#000"/>
:
可見,在 onDraw() 方法對padding處理之後,在布局檔案中無論怎麼設定padding,都能保證圓心在可用性區域域的中心。
首先在 res/values 路徑下建立一個xml檔案,添加一個設定圓的顏色的屬性:
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="MyView"> <attr name="circle_color" format="color"/> </declare-styleable></resources>
在構造方法中解析屬性
public MyView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.MyView); mColor = typeArray.getColor(R.styleable.MyView_circle_color, mColor); typeArray.recycle(); init();}
最後在布局檔案中這是屬性就可以了,要注意的是,在使用自訂屬性時要添加 xmlns:app="http://schemas.android.com/apk/res-auto" 才可以。
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="cn.codingblock.view.activity.MyViewActivity"> <cn.codingblock.view.reset_view.MyView android:id="@+id/myview" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="10dp" android:paddingLeft="15dp" android:paddingRight="30dp" app:circle_color="#ad42ce" android:background="#000"/></LinearLayout>
改變顏色後的如下:
為自訂View添加互動事件MotionEvent 觸摸事件
@Overridepublic boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(TAG, "onTouchEvent: ACTION_DOWN"); break; case MotionEvent.ACTION_UP: Log.i(TAG, "onTouchEvent: ACTION_UP"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, "onTouchEvent: ACTION_MOVE"); break; } return super.onTouchEvent(event);}
在自訂 View 中,重寫 onTouchEvent() 方法,擷取 MotionEvent,正如上面代碼所寫,MotionEvent 比較常用的事件有三種 ACTION_DOWN、ACTION_MOVE、ACTION_UP 分別對應手指按下-移動-離開。
接下來對上面的圓形demo添加一個小事件,就是每當手指點擊一下螢幕,圓形就隨機換一種顏色:
private Random mRandom = new Random(100);private int[] mColors = new int[] { Color.parseColor("#ff0000"), Color.parseColor("#ffffff"), Color.parseColor("#ff00ff"), Color.parseColor("#ffff00"), Color.parseColor("#ff00ff"), Color.parseColor("#0000ff")};@Overridepublic boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mColor = mColors[mRandom.nextInt(6)]; mPaint.setColor(mColor); invalidate(); // 通知控制項重繪 break; case MotionEvent.ACTION_UP: Log.i(TAG, "onTouchEvent: ACTION_UP"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, "onTouchEvent: ACTION_MOVE"); break; } return super.onTouchEvent(event);}
效果如下:
大家也可以在此基礎上稍微再擴充一下,例如:通過 event.getX() 和 event.getY() 擷取觸摸點的座標,判斷出點是否落在了圓形地區內,從而使只有點手指點到圓形地區內才改變顏色,否則不改變。感興趣的童鞋可自行動手試一試。
在上面代碼中通知 View 重繪時使用了 invalidate() 方法,其實 postInvalidate() 也可以通知 View 重繪,那麼這兩者有什麼區別呢?
其實簡單來說,invalidate() 只能在 UI 線程中使用,而 postInvalidate() 可以在子線程中使用。
ScaleGestureDetector 縮放手指檢測
除了上面最普通的 MotionEvent 事件之外,Android 還提供了很多有趣的事件,就想 GestureDetector(手勢檢測)、VelocityTracker(速度追蹤)等等,用起來也都很方便,其實只要你願意,這些事件也完全可以在 onTouchEvent() 方法中實現,接下來在為上述的圓形 Demo 添加一個縮放的功能,也就是使用 ScaleGestureDetector 實現,效果跟平時在手機查看照片時我們用兩根手指來放大/縮小圖片一樣。
ScaleGestureDetector 在使用起來也很簡單,首先需要初始化並為其添加一個放縮手勢監聽器,並且需要在 onTouchEvent() 方法內,通過 ScaleGestureDetector.onTouchEvent(event) 來讓 ScaleGestureDetector 接管觸摸事件,其餘的事項請注意看代碼中的注釋。
在上述代碼的基礎上新增如下代碼:
private Context mContext;private ScaleGestureDetector mScaleGestureDetector; // 縮放手勢檢測private float mScaleRate = 1; // 縮放比率private void init() { mPaint.setAntiAlias(true); // 消除鋸齒 mPaint.setColor(mColor); // 為畫筆設定顏色 // 初始化 ScaleGestureDetector 並添加縮放手勢監聽器 mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener);}@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaddingTop = getPaddingTop(); mPaddingBottom = getPaddingBottom(); mPaddingLeft = getPaddingLeft(); mPaddingRight = getPaddingRight(); // 可用寬度和寬度要考慮padding mUsableWidth = getWidth() - mPaddingRight - mPaddingLeft; mUsableHeight = getHeight() - mPaddingTop - mPaddingBottom; // 畫筆起始點要考慮padding mUsableStartX = mPaddingLeft; mUsableStartY = mPaddingTop; // 確定可用性區域域的中心為圓心 mCircleX = mUsableStartX + mUsableWidth / 2; mCircleY = mUsableStartY + mUsableHeight / 2; // 確定圓的半徑,以可用寬度和高度兩者較短的一半為圓的半徑 if (mUsableWidth <= mUsableHeight) { mCircleRadius = mUsableWidth / 2; } else { mCircleRadius = mUsableHeight / 2; } // 讓半徑乘以縮放倍率 mCircleRadius *= mScaleRate; canvas.drawCircle(mCircleX, mCircleY, mCircleRadius, mPaint);}@Overridepublic boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mColor = mColors[mRandom.nextInt(6)]; mPaint.setColor(mColor); invalidate(); // 通知控制項重繪 break; case MotionEvent.ACTION_UP: Log.i(TAG, "onTouchEvent: ACTION_UP"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, "onTouchEvent: ACTION_MOVE"); break; } // 讓縮放手勢檢測器接管觸摸事件 if (mScaleGestureDetector.onTouchEvent(event)) { return true; } return super.onTouchEvent(event);}private ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() { @Override public boolean onScale(ScaleGestureDetector detector) { Log.i(TAG, "onScale: " + detector.getScaleFactor()); // 擷取縮放比例因素並累乘到縮放倍率上 mScaleRate *= detector.getScaleFactor(); postInvalidate(); return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { Log.i(TAG, "onScaleBegin: " + detector.getScaleFactor()); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { Log.i(TAG, "onScaleEnd: " + detector.getScaleFactor()); }};
上面代碼需要注意的是,在 ScaleGestureDetector 捕獲到事件後要正確的將事件消費掉(注意代碼中返回 true 的地方),不然縮放手勢無法正常工作。
自訂 View 在 Android 中一直以來都是很重要的一部分,在平時的開發想要做出一個個性炫酷的互動介面是離不開自訂 View,自訂 View 說難不難,說簡單也不簡單,總之,千裡之行,始於足下,只要我們掌握好自訂 View 的基礎知識,再複雜的介面也可以一步步完成。
最後想說的是,本系列文章為博主對Android知識進行再次梳理,查缺補漏的學習過程,一方面是對自己遺忘的東西加以複習重新掌握,另一方面相信在重新學習的過程中定會有巨大的新收穫,如果你也有跟我同樣的想法,不妨關注我一起學習,互相探討,共同進步!
參考文獻:
- 《Android開發藝術探索》
- 《Android開發進階從小工到專家》
Android查缺補漏(View篇)--自訂 View 的基本流程