編輯:關於Android編程
對於一個Android攻城獅來說,自定義控件是一項必須掌握的重要技能點,然而對於大部分人而言,感覺自定義控件並不是那麼容易。在工作過程中難免遇到一些特效需要自己定義控件實現,如果你不會,內心會有強烈的挫敗感,這對一個程序員來說是決不能容忍的,接下來我將寫一系列博客,和大家一起學習自定義控件,讓她赤裸裸的站在我們的面前,讓我們為所欲為… :joy:
言歸正傳,接觸到一個類,你不太了解他,如果貿然翻閱源碼只會讓你失去方向,不知從哪裡下手;所以我們應該從文檔著手,看看它是個什麼東西,裡面有哪些屬性和方法,都是用來干嘛的。下面我們看看官方文檔對View的介紹:
View這個類代表用戶界面組件的基本構建塊。View在屏幕上占據一個矩形區域,並負責繪制和事件處理。View是用於創建交互式用戶界面組件(按鈕、文本等)的基礎類。它的子類ViewGroup是所有布局的父類,它是一個可以包含其他view或者viewGroup並定義它們的布局屬性的看不見的容器。
實現一個自定義View,你通常會覆蓋一些framework層在所有view上調用的標准方法。你不需要重寫所有這些方法。事實上,你可以只是重寫onDraw(android.graphics.Canvas)。
onFinishInflate()
當View和他的所有子控件被XML布局文件填充完成時被調用。(這個方法裡面可以完成一些初始化,比如初始化子控件)
布局
onMeasure(int, int)
當決定view和他的孩子的尺寸需求時被調用(也就是測量控件大小時調用)
onLayout(boolean, int, int, int, int)
當View給他的孩子分配大小和位置的時候調用(擺放子控件)
onSizeChanged(int, int, int, int)
當view大小發生變化時調用
繪制
onDraw(android.graphics.Canvas)
當視圖應該呈現其內容時調用(繪制)
事件處理
onKeyDown(int, KeyEvent)
按鍵時被調用
onKeyUp(int, KeyEvent)
按鍵被抬起時調用
onTrackballEvent(MotionEvent)
Called when a trackball motion event occurs.
onTouchEvent(MotionEvent)
觸摸屏幕時調用
焦點
onFocusChanged(boolean, int, android.graphics.Rect)
獲取到或者失去焦點是調用
onWindowFocusChanged(boolean)
窗口獲取或者失去焦點是調用
Attaching
onAttachedToWindow()
當視圖被連接到一個窗口時調用
onDetachedFromWindow()
當視圖從窗口分離時調用
onWindowVisibilityChanged(int)
當View的窗口的可見性發生改變時調用
從上面官方文檔介紹我們可以知道,View是所有控件(包括ViewGroup)的父類,它裡面有一些常見的方法(上表),如果我們要自定義View,最簡單的只需要重寫onDraw(android.graphics.Canvas)即可,聽起來是不是很簡單?那我們就動手自定義一個屬於自己的TextView吧。
創建一個類MyTextView繼承View,發現報錯,因為要覆蓋他的構造方法(因為View中沒有參數為空的構造方法),View有四種形式的構造方法,其中四個參數的構造方法是API 21才出現,所以一般我們只需要重寫其他三個構造方法即可。它們的參數不一樣分別對應不同的創建方式,比如只有一個Context參數的構造方法通常是通過代碼初始化控件時使用;而兩個參數的構造方法通常對應布局文件中控件被映射成對象時調用(需要解析屬性);通常我們讓這兩個構造方法最終調用三個參數的構造方法,然後在第三個構造方法中進行一些初始化操作。
public class MyView extends View {
/**
* 需要繪制的文字
*/
private String mText;
/**
* 文本的顏色
*/
private int mTextColor;
/**
* 文本的大小
*/
private int mTextSize;
/**
* 繪制時控制文本繪制的范圍
*/
private Rect mBound;
private Paint mPaint;
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化
mText = "Udf32fA";
mTextColor = Color.BLACK;
mTextSize = 100;
mPaint = new Paint();
mPaint.setTextSize(mTextSize);
mPaint.setColor(mTextColor);
//獲得繪制文本的寬和高
mBound = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), mBound);
}
//API21
// public MyTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// super(context, attrs, defStyleAttr, defStyleRes);
// init();
// }
@Override
protected void onDraw(Canvas canvas) {
//繪制文字
canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
}
}
布局文件:
運行結果:
vcHL0MK1xNaqyra146O619S2qNLlyvTQ1DwvcD4NCjxwPiZuYnNwOzwvcD4NCjxoMiBpZD0="2-自定義屬性">2. 自定義屬性
在res/values/下創建一個名為attrs.xml的文件,然後定義如下屬性:
format的意思是該屬性的取值是什麼類型(支持的類型有string,color,demension,integer,enum,reference,float,boolean,fraction,flag)
然後在布局文件中使用自定義屬性,記住一定要引入我們的命名空間xmlns:openxu="http://schemas.android.com/apk/res-auto"
在構造方法中獲取自定義屬性的值:
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//獲取自定義屬性的值
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView, defStyleAttr, 0);
mText = a.getString(R.styleable.MyTextView_mText);
mTextColor = a.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);
mTextSize = a.getDimension(R.styleable.MyTextView_mTextSize, 100);
mPaint = new Paint();
mPaint.setTextSize(mTextSize);
mPaint.setColor(mTextColor);
//獲得繪制文本的寬和高
mBound = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), mBound);
}
運行結果:
通過運行結果,我們已經成功為MyTextView定義了屬性,並獲取到值,至於自定義屬性的詳細知識點到後面會專門寫一篇博客去介紹。
到此為止,發現自定義控件還是比較簡單的嘛。看看結果,跟原生的TextView還有什麼差別?接下來做一點小變化:
讓繪制的文本長一點openxu:mText="i love you i love you i love you"
,運行結果:
有沒有發現不和諧的現象?文本超度超出了控件邊界,控件太小,不足以顯示辣麼長的文本,我們將寬高改為wrap_content
試試:
什麼鬼?不是包裹內容嗎?怎麼填充整個屏幕了?根據頂部官方文檔的說明,我們猜想肯定是控件的測量onMeasure方法出了問題,接下來我們學習onMeasure方法。
在學習onMasure方法之前,我們要先了解他的參數中的一個類MeasureSpec,知己知彼才能百戰百勝 。
跟蹤一下源碼,發現它是View中的一個靜態內部類,是由尺寸和模式組合而成的一個值,用來描述父控件對子控件尺寸的約束,看看他的部分源碼,一共有三種模式,然後提供了合成和分解的方法:
/**
* measurespec封裝了父控件對他的孩子的布局要求。
* 一個measurespec由大小和模式。有三種可能的模式:
*/
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//父控件不強加任何約束給子控件,它可以是它想要任何大小。
public static final int UNSPECIFIED = 0 << MODE_SHIFT; //0
//父控件決定給孩子一個精確的尺寸
public static final int EXACTLY = 1 << MODE_SHIFT; //1073741824
//父控件會給子控件盡可能大的尺寸
public static final int AT_MOST = 2 << MODE_SHIFT; //-2147483648
/**
* 根據給定的尺寸和模式創建一個約束規范
*/
public static int makeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
/**
* 從約束規范中獲取模式
*/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* 從約束規范中獲取尺寸
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
這樣說起來還是有點抽象,舉一個小栗子大家就知道這三種約束到底是什麼意思。我們自定義一個View,為了方便起見,讓它繼承Button,布局文件中設置不同的寬高條件,然後在onMeasure方法中打印一下他的參數(int widthMeasureSpec, int heightMeasureSpec)到底是個什麼鬼
/**
* Created by openXu on 16/5/19.
*/
public class MyView extends Button {
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //獲取寬的模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec); //獲取高的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //獲取寬的尺寸
int heightSize = MeasureSpec.getSize(heightMeasureSpec); //獲取高的尺寸
Log.v("openxu", "寬的模式:"+widthMode);
Log.v("openxu", "高的模式:"+heightMode);
Log.v("openxu", "寬的尺寸:"+widthSize);
Log.v("openxu", "高的尺寸:"+heightSize);
}
}
情形1,讓按鈕包裹內容:
log打印:
05-19 06:02:55.112: V/openxu(15599): 寬的模式:-2147483648
05-19 06:02:55.112: V/openxu(15599): 高的模式:-2147483648
05-19 06:02:55.112: V/openxu(15599): 寬的尺寸:1080
05-19 06:02:55.112: V/openxu(15599): 高的尺寸:1860
情形2,讓按鈕填充父窗體:
log打印:
05-19 06:05:37.302: V/openxu(15960): 寬的模式:1073741824
05-19 06:05:37.302: V/openxu(15960): 高的模式:1073741824
05-19 06:05:37.302: V/openxu(15960): 寬的尺寸:1080
05-19 06:05:37.302: V/openxu(15960): 高的尺寸:1860
情形3,給按鈕的寬設置為具體的值:
log打印:
05-19 06:07:48.932: V/openxu(16105): 寬的模式:1073741824
05-19 06:07:48.932: V/openxu(16105): 高的模式:-2147483648
05-19 06:07:48.932: V/openxu(16105): 寬的尺寸:300
05-19 06:07:48.932: V/openxu(16105): 高的尺寸:1860
根據上面的測試,我們發現,約束中分離出來的尺寸就是父控件剩余的寬高大小(除了設置具體的寬高值外);而幾種約束中的模式不就是對應我們在布局文件中設置給按鈕的幾種情況嗎?如下:
UNSPECIFIED
0
布局文件好像必須設置寬高,目前還沒找到與之對應的布局參數,使用較少
EXACTLY
match_parent
/具體寬高值
1073741824
如果是填充父窗體,說明父控件已經明確知道子控件想要多大的尺寸了(就是剩余的空間都要了)
AT_MOST
wrap_content
-2147483648
包裹內容就是父窗體並不知道子控件到底需要多大尺寸(具體值),需要子控件自己測量之後再讓父控件給他一個盡可能大的尺寸以便讓內容全部顯示
通過上面對MeasureSpec的了解,我們現在就有能看懂View的onMeasure方法默認是怎樣為控件測量大小的了
看View中onMeasure的源碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
onMeasure方法調用了setMeasuredDimension(int measuredWidth, int measuredHeight)方法,而傳入的參數已經是測量過的默認寬和高的值了;我們看看getDefaultSize 方法是怎麼計算測量寬高的。根據父控件給予的約束,發現AT_MOST (相當於wrap_content )和EXACTLY (相當於match_parent )兩種情況返回的測量寬高都是specSize,而這個specSize正是我們上面說的父控件剩余的寬高,所以默認onMeasure方法中wrap_content 和match_parent 的效果是一樣的,都是填充剩余的空間。
我們先忽略掉UNSPECIFIED 的情況(使用極少),只考慮AT_MOST 和EXACTLY ,現在的問題是設置wrap_content 時,控件卻使用了match_parent 的效果,看下面怎麼重寫onMeasure(注釋比較詳細,不做過多講解):
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //獲取寬的模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec); //獲取高的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //獲取寬的尺寸
int heightSize = MeasureSpec.getSize(heightMeasureSpec); //獲取高的尺寸
Log.v("openxu", "寬的模式:"+widthMode);
Log.v("openxu", "高的模式:"+heightMode);
Log.v("openxu", "寬的尺寸:"+widthSize);
Log.v("openxu", "高的尺寸:"+heightSize);
int width;
int height ;
if (widthMode == MeasureSpec.EXACTLY) {
//如果match_parent或者具體的值,直接賦值
width = widthSize;
} else {
//如果是wrap_content,我們要得到控件需要多大的尺寸
float textWidth = mBound.width(); //文本的寬度
//控件的寬度就是文本的寬度加上兩邊的內邊距。內邊距就是padding值,在構造方法執行完就被賦值
width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
Log.v("openxu", "文本的寬度:"+textWidth + "控件的寬度:"+width);
}
//高度跟寬度處理方式一樣
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
float textHeight = mBound.height();
height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
Log.v("openxu", "文本的高度:"+textHeight + "控件的高度:"+height);
}
//保存測量寬度和測量高度
setMeasuredDimension(width, height);
}
布局文件:
下面是輸出的log:
05-19 01:29:12.662 27380-27380/view.openxu.com.mytextview V/openxu: 寬的模式:-2147483648
05-19 01:29:12.662 27380-27380/view.openxu.com.mytextview V/openxu: 高的模式:-2147483648
05-19 01:29:12.662 27380-27380/view.openxu.com.mytextview V/openxu: 寬的尺寸:720
05-19 01:29:12.666 27380-27380/view.openxu.com.mytextview V/openxu: 高的尺寸:1230
05-19 01:29:12.678 27380-27380/view.openxu.com.mytextview V/openxu: 文本的寬度:652.0控件的寬度:732
05-19 01:29:12.690 27380-27380/view.openxu.com.mytextview V/openxu: 文本的高度:49.0控件的高度:129
我的模擬器是720x1280的,根據log顯示,文本的寬度是652,加上兩邊的內邊距,控件的寬度為732,確實實現了包裹內容的效果,運行程序結果如下:
但是發現寬度已經超出了屏幕,還不能像TextView一樣換行;下面我們簡單的模擬一下換行的功能,做的不夠好,但有這個效果,不是重點,不需要重點掌握
只需要在測量的時候,根據文字的總長度和控件的寬度,就可以知道需要繪制幾行,然後將文本分割成小段放入集合中,在onDraw方法中分別繪制;
需要注意的是,onMeasure方法不只調用一次,所以在分段文本是需要判斷,不要重復分段,否則會報錯。代碼如下(僅供參考):
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import java.util.ArrayList;
/**
* Created by openXu on 16/5/19.
*/
public class MyTextView extends View {
/**
* 需要繪制的文字
*/
private String mText;
private ArrayList mTextList;
/**
* 文本的顏色
*/
private int mTextColor;
/**
* 文本的大小
*/
private float mTextSize;
/**
* 繪制時控制文本繪制的范圍
*/
private Rect mBound;
private Paint mPaint;
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTextList = new ArrayList();
//獲取自定義屬性的值
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView, defStyleAttr, 0);
mText = a.getString(R.styleable.MyTextView_mText);
mTextColor = a.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);
mTextSize = a.getDimension(R.styleable.MyTextView_mTextSize, 100);
Log.v("openxu", "文本總長度:"+mText);
mPaint = new Paint();
mPaint.setTextSize(mTextSize);
mPaint.setColor(mTextColor);
//獲得繪制文本的寬和高
mBound = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), mBound);
}
//API21
// public MyTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// super(context, attrs, defStyleAttr, defStyleRes);
// init();
// }
@Override
protected void onDraw(Canvas canvas) {
//繪制文字
for(int i = 0; i
布局文件:
運行效果:
到此為止,我們已經了解到自定義控件的基本步驟:
1. 繼承View,重寫構造方法
2. 自定義屬性,在構造方法中初始化屬性
3. 重寫onMeasure方法測量寬高
4. 重寫onDraw方法繪制控件
各位看官,看到這裡辛苦了,順便留個言、點個贊呗~ ~謝過了
源碼下載:
https://github.com/openXu/MyTextView
Google的開源Android移動操作系統正在席卷全球智能手機市場,和蘋果不一樣,它對那些想將應用程序提交到iPhone App Store的開發人員有著嚴格的指導方針
自定義View通訊錄字母快速索引在Android日常開發中,我們經常在聯系人界面看到一些字母導航欄,點擊字母的時候,會根據漢字的首拼音來查找是否存在相應的item,這種效
實現了浏覽器的返回 前進 主頁 退出 輸入網址的功能注釋的很清楚啦 就不多說了首先是布局文件 <linearlayout xmlns:android=&q
導航抽屜(navigationdrawer)是一個從屏幕左邊滑入的面板,用於顯示應用的主要導航項目。用戶可以通過在屏幕左邊緣滑入或者觸摸操作欄的應用圖標打開導航抽屜。導航