擴展已存在的視圖、組建複合的控件以及建立獨特的新視圖(定製控件),能夠建立出最適合本身的應用程序工做流的優美的用戶界面。Android運行從已有的視圖工具箱派生子類或實現本身的視圖控件,從而能夠自由調整用戶界面。html
建立新視圖的最佳方法與但願達到的目標有關:java
1. 若是現有控件已經能夠知足但願實現的基本功能,那麼就只須要對現有控件的外觀和行爲進行修改擴展便可。經過重寫事件處理程序和onDraw方法,可是仍然回調超類的方法,能夠對視圖進行定製,而沒必要重現實現它的功能。android
2. 能夠經過組合多個視圖來建立不可分割的、可重用的控件,從而使它能夠綜合使用多個相互關聯的視圖的功能。canvas
3. 當須要一個全新的界面,而經過修改或組合現有控件不能實現該目標時,就能夠建立一個全新的控件。app
1 修改現有視圖(繼承Android SDK中的基本控件)ide
Android小組件工具箱包含的視圖提供了不少建立UI必需的控件,但這些控件一般都是很通用的。要在一個已有控件的基礎上建立一個新的視圖,就須要建立一個擴展了原控件的新類。函數
要修改新視圖的外觀或者行爲,只要重寫和擴展與但願修改的行爲相關的事件處理程序便可。工具
繼承現有Android SDK中的控件,實現自定義功能。佈局
通常須要覆寫onDraw(),用以實現不同的佈局。this
package com.demo.view; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.view.KeyEvent; import android.widget.TextView; import com.demo.R; public class TextViewDesign extends TextView { private Paint mMarginPaint; private Paint mLinePaint; private int mPaperColor; private float mMargin; public TextViewDesign(Context context) { super(context); } public TextViewDesign(Context context, AttributeSet attrs) { super(context, attrs); // XML文件中引入時,調用2參數構造方法 initData(); } public TextViewDesign(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onDraw(Canvas canvas) { // 設置繪製頁面的顏色 canvas.drawColor(mPaperColor); // 畫item以前的垂直線 canvas.drawLine(0, 0, 0, getMeasuredHeight(), mLinePaint); // 畫item之間的分割線 canvas.drawLine(0, getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight(), mLinePaint); // 畫文字前的垂直線 canvas.drawLine(mMargin, 0, mMargin, getMeasuredHeight(), mMarginPaint); canvas.save(); // 畫布平移 canvas.translate(mMargin, 0); // 使用TextView基類渲染文本 super.onDraw(canvas); canvas.restore(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // 使用TextView基類所實現的功能來響應鍵盤事件 return super.onKeyDown(keyCode, event); } /** * <功能描述> 初始化自定義控件數據 * * @return void [返回類型說明] */ private void initData() { // 得到對資源表的引用 Resources contentResources = getResources(); mMarginPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mMarginPaint .setColor(contentResources.getColor(R.color.notepad_margin)); mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mLinePaint.setColor(contentResources.getColor(R.color.notepad_lines)); mPaperColor = contentResources.getColor(R.color.notepad_paper); mMargin = contentResources.getDimension(R.dimen.notepad_margin); } }
實現更改後的視圖以下:
對比以前的實現UI:
自定義以後的UI視圖,有了很大的區別。
2 建立複合控件(繼承Android SDK中的佈局)
複合控件是指不可分割的、自包含的視圖組,其中包含了多個排列和鏈接在一塊兒的子視圖。
建立複合控件時,必須對它包含的視圖的佈局、外觀和交互進行定義。複合控件是經過擴展一個ViewGroup(一般是一個佈局)來建立的。所以,要建立一個新的複合控件,首先須要選擇一個適合放置子控件的佈局類,而後擴展該類。
package com.demo.view; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import com.demo.R; public class ClearableEditText extends LinearLayout { private EditText mEditText; private Button mBtnClear; public ClearableEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public ClearableEditText(Context context, AttributeSet attrs) { super(context, attrs); } public ClearableEditText(Context context) { super(context); initView(); } private void initView() { String infService = Context.LAYOUT_INFLATER_SERVICE; LayoutInflater li; li = (LayoutInflater) getContext().getSystemService(infService); li.inflate(R.layout.clearable_edit, this, true); mEditText = (EditText) findViewById(R.id.et_content); mBtnClear = (Button) findViewById(R.id.btn_clear); hookupButton(); } /** * <功能描述> 清除EditText內容 * * @return void [返回類型說明] */ private void hookupButton() { mBtnClear.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mEditText.setText(""); } }); } }
上述的ViewGroup通常是一個佈局:Layout子類,同時接受一個.xml佈局文件。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > <EditText android:id="@+id/et_content" android:layout_width="match_parent" android:layout_height="wrap_content" /> <Button android:id="@+id/btn_clear" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Clear" /> </LinearLayout>
3 建立定製的視圖
建立全新的視圖將從根本上決定應用程序的樣式以及觀感的能力。經過建立本身的控件,能夠建立出知足本身需求的獨特的UI。
要在一個空畫布上建立新的控件,就須要對View類或者SurfaceView類進行擴展。View類提供了一個Canvas對象和一系列繪製方法以及Paint類。以後,能夠重寫像屏幕觸摸或者按鍵按下這樣的用戶事件以提供交互。
在那些不要求3D圖像和極快地從新回執界面的狀況下,View基類提供了一個強大的輕量級解決方案。
SurfaceView提供了一個支持從後臺線程繪製而且可使用OpenGL來繪製圖形的Surface對象。對於那些對圖形要求很高的控件,特別是遊戲和3D可視化來講是一個很好的解決方案。
View基類呈現出第一個清晰的100*100像素的空白正方形。要改變控件的大小並呈現出一個更加吸引人的可視界面,就須要分別對onMeasure和onDraw方法進行重寫。onMeasure方法中,新的視圖將會計算出一系列給定的邊界條件下佔據的高度和寬度;onDraw用於在畫布上繪圖。
3.1. 建立新的可視視圖
建立繼承View的類,並在其中覆寫相關的方法,好比:onMeasure()、onDraw()等
3.2. 繪製控件
onDraw()是繪製控件的地方。若是想要建立一個全新的可視界面,那麼能夠嘗試從頭建立一個新的小組件。onDraw方法中的Canvas參數就是用來進行繪製的表面(畫布)。
可使用繪製的工具類包括:Canvas、Paint、Drawable,控件可悲渲染的複雜度和細節會受到屏幕的大小和渲染它的處理器的能力的限制。
在Android中編寫高效代碼的最重要的技術之一是避免重複地建立和銷燬對象。在onDraw方法中建立的任何對象都會在屏幕刷新的時候被建立和銷燬。能夠經過將盡量多的這樣的對象的做用域限定爲類做用域,並將它們的建立過程交給構造函數來完成,以此來提升效率。
3.3. 調整控件大小
除非所要求的控件老是剛好佔據100*100像素,不然將須要重寫onMeasure()。
當控件的父容器佈局它的子控件的時候,就會調用onMeasure()。提出「你須要使用多大的空間?」,同時傳入兩個參數:widthMeasureSpec和heightMeasureSpec。這兩個參數指定了控件可用的控件以及一些描述這些空間的元數據。
最後把視圖的高度和寬度傳遞給setMeasuredDimension()中。
package com.demo.view; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.View; public class DesignView extends View { public DesignView(Context context, AttributeSet attrs, int defStyleAttr) { // 使用資源文件進行填充時必需的構造函數 super(context, attrs, defStyleAttr); } public DesignView(Context context, AttributeSet attrs) { // 使用資源文件進行填充時必需的構造函數 super(context, attrs); } public DesignView(Context context) { // 使用代碼進行建立時必需的構造函數 super(context); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int measuredHeight = measureHeight(widthMeasureSpec); int measuredWidth = measureWidth(heightMeasureSpec); // 必需調用該方法 setMeasuredDimension(measuredWidth, measuredHeight); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } /** * <功能描述> 解碼參數值,並計算View高度 * * @param measureSpec * @return [參數說明] * @return int [返回類型說明] */ private int measureHeight(int measureSpec) { int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); /** * 計算View的高度 */ return specSize; } /** * <功能描述> 解碼參數值,並計算View寬度 * * @param measureSpec * @return [參數說明] * @return int [返回類型說明] */ private int measureWidth(int measureSpec) { int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); /** * 計算View的高度 */ return specSize; } }
從效率的角度來考慮,邊界參數widthMeasureSpec和heightMeasureSpec是做爲整數傳入的。
AT_MOST表示的是:控件可用的最大空間;說明父控件正在詢問視圖在給定上界的狀況下但願佔據的空間的大小。
EXACTLY:控件佔據的確切大小;說明視圖被放置到了指定確切大小的空間中。
UNSPECIFIED:控件沒有獲得任何關於size所表明的引用。
3.4. 處理用戶交互事件
要使新視圖是可交互的,就須要讓View可以對用戶事件做出反應,例如按下按鍵、觸摸屏幕等等。Android提供了多個虛擬事件處理程序,能夠對用戶輸入做出反應。
@Override public boolean onKeyDown(int keyCode, KeyEvent event) { // 若是事件獲得處理,則返回true return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { // 若是事件獲得處理,則返回true return super.onKeyUp(keyCode, event); } @Override public boolean onTrackballEvent(MotionEvent event) { // 得到事件所表明的類型 int actionPerformed = event.getAction(); // 若是事件獲得處理,則返回true return super.onTrackballEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { // 得到事件所表明的類型 int actionPerformed = event.getAction(); // 若是事件獲得處理,則返回true return super.onTouchEvent(event); }
實例分析:建立一個羅盤View
Android SDK提供的原生控件中沒有該View,須要自定義或是定製View。
package com.demo.view; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; import com.demo.LogUtil; import com.demo.R; public class CompassView extends View { private static final String TAG = CompassView.class.getSimpleName(); private float mBearing; private Paint mMarkerPaint; private Paint mTextPaint; private Paint mCirclePaint; private String mNorthString; private String mEastString; private String mSouthString; private String mWestString; private int mTextHeight; public CompassView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initCompassView(); } public CompassView(Context context, AttributeSet attrs) { super(context, attrs); initCompassView(); } public CompassView(Context context) { super(context); initCompassView(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Compass須要佔據儘量多的空間,經過設置最短的邊界、高度或者寬度來設置測量的尺寸 int measuredWidth = measure(widthMeasureSpec); int measuredHeight = measure(heightMeasureSpec); // 設置最短的邊界 int d = Math.min(measuredWidth, measuredHeight); setMeasuredDimension(d, d); LogUtil.d(TAG, "onMeasure::measuredWidth=" + measuredWidth + "; measuredHeight=" + measuredHeight + "; d=" + d); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int width = getMeasuredWidth(); int height = getMeasuredHeight(); LogUtil.d(TAG, "onDraw::width=" + width + "; height=" + height); int px = width / 2; int py = height / 2; int radius = Math.min(px, py); // 繪製羅盤邊界,爲背景着色 canvas.drawCircle(px, py, radius, mCirclePaint); // 繪製旋轉,讓當前方向老是指向設備頂部 canvas.rotate(-mBearing, px, py); // 返回字符串的寬度 int textWidth = (int) mTextPaint.measureText("W"); LogUtil.d(TAG, "textWidth=" + textWidth); // 11 int cardinalX = px - textWidth / 2; // 265 int cardinalY = py - radius + mTextHeight; // 13 LogUtil.d(TAG, "onDraw::cardinalX=" + cardinalX + "; cardinalY=" + cardinalY); for (int i = 0; i < 24; i++) { // 將360等份平均分爲24份,旋轉24次(每份15°) // 畫標尺線 canvas.drawLine(px, py - radius, px, py - radius + 10, mMarkerPaint); canvas.save(); // Y方向平移 canvas.translate(0, mTextHeight); if (i % 6 == 0) { // 每旋轉90° String dirString = ""; switch (i) { case 0: { // 正北 dirString = mNorthString; int arrowY = 2 * mTextHeight; // 畫箭頭 canvas.drawLine(px, arrowY, px - 5, 3 * mTextHeight, mMarkerPaint); canvas.drawLine(px, arrowY, px + 5, 3 * mTextHeight, mMarkerPaint); } break; case 6: { // 正東 dirString = mEastString; } break; case 12: { // 正南 dirString = mSouthString; } break; case 18: { // 正西 dirString = mWestString; } break; default: break; } // 寫方向標識 canvas.drawText(dirString, cardinalX, cardinalY, mTextPaint); } else if (i % 3 == 0) { // 每旋轉45° String angle = String.valueOf(i * 15); float angleTextWidth = mTextPaint.measureText(angle); int angleTextX = (int) (px - angleTextWidth / 2); int angleTextY = py - radius + mTextHeight; // 寫角度標識 canvas.drawText(angle, angleTextX, angleTextY, mTextPaint); } canvas.restore(); canvas.rotate(15, px, py); } canvas.restore(); } public void setBearing(float _bearing) { mBearing = _bearing; } public float getBearing() { return mBearing; } /** * <功能描述> 計算尺寸 * * @param measureSpec * @return [參數說明] * @return int [返回類型說明] */ private int measure(int measureSpec) { int result = 0; // 解析參數 int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.UNSPECIFIED) { // 沒有指定界限,返回默認值200 result = 200; } else { // 但願填充可用控件,返回整個可用空間 result = specSize; } LogUtil.d(TAG, "measure::result=" + result); return result; } /** * <功能描述> 初始化View * * @return void [返回類型說明] */ private void initCompassView() { setFocusable(true); Resources res = this.getResources(); mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCirclePaint.setColor(res.getColor(R.color.background_color)); mCirclePaint.setStrokeWidth(1); mCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE); mNorthString = res.getString(R.string.cardinal_north); mEastString = res.getString(R.string.cardinal_east); mWestString = res.getString(R.string.cardinal_west); mSouthString = res.getString(R.string.cardinal_south); mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(res.getColor(R.color.text_color)); mTextHeight = (int) mTextPaint.measureText("yY"); LogUtil.d(TAG, "initCompassView::mTextHeight=" + mTextHeight); // 13 mMarkerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mMarkerPaint.setColor(res.getColor(R.color.marker_color)); } }
定製View的使用:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.demo.CompassActivity" > <com.demo.view.CompassView android:id="@+id/view_compass" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
package com.demo; import android.app.Activity; import android.os.Bundle; import com.demo.view.CompassView; public class CompassActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); CompassView compassView = (CompassView) findViewById(R.id.view_compass); compassView.setBearing(45); } }
當前指向的方向爲:N-->E偏角爲45°
總結:View的定製,是在Canvas中一步步繪製出來的。
疑問:rotate()和drawLine()的衝突問題。