一、Android控件架構(gòu)
Android的每個控件都是占一塊矩形的區(qū)域晕城,大致的分兩類泞坦,繼承View和ViewGroup,ViewGroup相當于一個容器砖顷,他可以管理多個子View贰锁,整個界面上的控件形成了一個樹形結(jié)構(gòu),也就是我們常說的控件樹滤蝠,上層控件負責下層控件的測量和繪制豌熄,并且傳遞交互事件,通過findviewbyid()這個方法來獲取物咳,其實就是遍歷查找锣险,在樹形圖的頂部都有一個ViewParent對象,這就是控制核心,所有的交互管理事件都是由它統(tǒng)一調(diào)度和分配芯肤,從而進行整個視圖的控制
通常情況下夯接,我們要顯示一個activity的視圖,需要使用setContentView()方法纷妆,那么這個方法到底做了些什么尼盔几?我們先來看看Android 體系的架構(gòu)圖
我們可以看到,每個activity都有一個window對象掩幢,在Android中逊拍,window對象通常由一個Phonewindow去實現(xiàn)的,phonewindow將一個DecorView設(shè)置為整個窗口的根View际邻,DecorView作為窗口界面的頂層視圖芯丧,封裝了一些通用的方法,可以說世曾,DecorView將要顯示的內(nèi)容都給了phonewindow缨恒,這里面所有的View監(jiān)聽,都是通過WindowManagerService來接收的轮听,通過相應的回調(diào)來OnClicListener骗露,在顯示上,他將屏幕分成了兩部分血巍,一個title一個content萧锉,看到這里,大家應該能看到一個熟悉的界面ContentView述寡,它是一個ID為content分framelayout柿隙,activity_main.xml就是設(shè)置在這個framelayout里面,我們可以看下圖:
而在代碼中當程序onCreate()時鲫凶,也就設(shè)置了layout,執(zhí)行完后禀崖,activitymanagerservice會直接調(diào)用onResume,這個時候系統(tǒng)會把整個DecorView添加到Phonewindow,然后顯示出來完成最后的繪制,所以是在onResume才完全初始化好View螟炫。
二波附、View的測量
我們想要繪制一個View,首先還是得知道這個View的大小不恭,系統(tǒng)是如何把他繪制出來的叶雹,在Android中,我們要想繪制一個View换吧,就必須要知道這個View的大小折晦,然后告訴系統(tǒng),這個過程在onMeasure()中進行
/**
* 測量
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Android給我們提供了一個設(shè)計短小精悍的類——MeasureSpec類沾瓦,通過他來幫助我們測量View, MeasureSpec是一個32位的int值满着,其中高2位為測量模式谦炒,低30為測量的大小,在計算中使用位運算時為了提高并且優(yōu)化效率风喇。
測量模式:
- EXACTLY:表示父視圖希望子視圖的大小應該是由specSize的值來決定的宁改,系統(tǒng)默認會按照這個規(guī)則來設(shè)置子視圖的大小,開發(fā)人員當然也可以按照自己的意愿設(shè)置成任意的大小魂莫。
- AT_MOST:表示子視圖最多只能是specSize中指定的大小还蹲,開發(fā)人員應該盡可能小得去設(shè)置這個視圖,并且保證不會超過specSize耙考。系統(tǒng)默認會按照這個規(guī)則來設(shè)置子視圖的大小谜喊,開發(fā)人員當然也可以按照自己的意愿設(shè)置成任意的大小。
- UNSPECIFIED:表示開發(fā)人員可以將視圖按照自己的意愿設(shè)置成任意的大小倦始,沒有任何限制斗遏。這種情況比較少見,自定義View的時候才會用到鞋邑。
View默認的onMeasure只支持EXACTLY模式诵次,所以如果在自定義控件的時候不重寫這個方法的話,也就只能使用EXACTLY模式了枚碗,控件可以響應你制定的具體的寬高值或者match_parent屬性逾一,如果我們自定義View要讓他支持 wrap_content,那就必須重寫onMeasure指定wrap_content時的大小视译。
通過MeasureSpec這個類嬉荆,我們就獲取到了View的測量模式和繪制的大小,有了這些信息我們就可以控制View顯示的大小了酷含,接下來,我們可以看一個簡單的小例子汪茧,我們重寫onMeasure這個方法。
/**
* 測量
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
查看super.onMeasure()這個方法舱污,可以發(fā)現(xiàn)呀舔,系統(tǒng)最終還是會調(diào)用setMeasuredDimension()這個方法將測量的寬高設(shè)置進去從而完成測量工作
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{ setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
通過源碼我們知道媚赖,我們自定義的高寬去設(shè)置捻撑,下面我們通過這個例子,來講一下自定義的測量值
第一步,我們從MeasureSpec類中提取出具體的測量模式和大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
然后我們根據(jù)模式給出不同的測量值鳖枕,當specMode為EXACTLY時耕魄,直接使用指定的specSize,當為其他兩種模式地時候,我們就需要一個默認的值了考润,特別是指定包裹內(nèi)容的時候罚舱,即AT_MOST模式包个,measureWidth()方法是這樣的:
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = 200;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
寬高的設(shè)置方法是一樣的,所以州既,當我們在布局中設(shè)置match_parent他就鋪滿容器了序臂,要是設(shè)置wrap_content构订,就是包裹內(nèi)容,如果不設(shè)置的話,那他就只有200的大小了
三、View的繪制
當我們用wrap_content方法測量完成之后卑吭,我們就該重寫onDraw()方法來繪制了,這個應該大家都很熟悉吧豆赏,首先我們要知道2D繪制的一些相關(guān)API
Canvas:顧名思義,畫布的意思富稻,而onDraw()就一個參數(shù)掷邦,就是Canvas了,我們要在其他地方繪制的話椭赋,就需要new對象了
Canvas canvas = new Canvas(Bitmap);
這個Bitmap是和Canvas緊密聯(lián)系的抚岗,這個過程我們稱之為裝載畫布,這個bitmap用來儲存畫布的一些像素信息哪怔,而且我們可以用canvas.drawxxx()來繪制相關(guān)的基礎(chǔ)圖形宣蔚,詳情可以看下
//繪制直線
canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint);
//繪制矩形
canvas.drawRect(float left, float top, float right, float bottom, Paint paint);
//繪制圓形
canvas.drawCircle(float cx, float cy, float radius, Paint paint);
//繪制字符
canvas.drawText(String text, float x, float y, Paint paint);
//繪制圖形
canvas.drawBirmap(Bitmap bitmap, float left, float top, Paint paint);
四向抢、自定義View
自定義View一直是個難點,Android自帶的控件很難滿足我們的需求胚委,所欲我們需要重寫控件或者自定義一個View挟鸠,但是一般強大的View,都還是存在少許的bug的亩冬,而且現(xiàn)在Android ROM的多樣性艘希,適配問題也越來越麻煩了。
在View中通常有以下比較重要的回調(diào)方法:
- onFinishInflate()
//從XML加載組件后回調(diào)
@Override
protected void onFinishInflate() {
// TODO Auto-generated method stub
super.onFinishInflate();
}
- onSizeChanged()
//組件大小改變時回調(diào)
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// TODO Auto-generated method stub
super.onSizeChanged(w, h, oldw, oldh);
}
- onMeasure()
// 回調(diào)該方法進行測量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
- onLayout()
// 回調(diào)該方法來確定顯示的位置
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
// TODO Auto-generated method stub
super.onLayout(changed, left, top, right, bottom);
}
- onTouchEvent()
// 監(jiān)聽到觸摸時間時回調(diào)
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
return super.onTouchEvent(event);
}
- onDraw()
// 繪圖
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
}
上面的方法并不需要全部寫出來硅急,看個人需要覆享,一般我們實現(xiàn)自定義控件有三種方法:
- 對現(xiàn)有的控件進行擴展
- 通過組件來實現(xiàn)新的控件
- 重寫View來實現(xiàn)全新的控件
1.對現(xiàn)有組件進行拓展
我們來分析一下這個效果,其實就是兩層的繪制营袜,我們依然要使用onDraw()撒顿,程序super.onDraw(canvas);方法來實現(xiàn)原生控件的功能,但是在調(diào)用super.onDraw(canvas)之前和之后都可以實現(xiàn)自己的邏輯
@Override
protected void onDraw(Canvas canvas) {
//在回調(diào)父類之前荚板,實現(xiàn)自己的邏輯凤壁,對textview來說就是繪制文本內(nèi)容前
super.onDraw(canvas);
//在回調(diào)父類之后,實現(xiàn)自己的邏輯啸驯,對textview來說就是繪制文本內(nèi)容后
}
//實例化畫筆1
paint1 = new Paint();
//設(shè)置顏色
paint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
//設(shè)置style
paint1.setStyle(Paint.Style.FILL);
//同上
paint2 = new Paint();
paint2.setColor(Color.YELLOW);
paint2.setStyle(Paint.Style.FILL);
最重要的部分就是什么時候調(diào)用super了客扎,然后我們開始繪制
//繪制外層
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint1);
//繪制內(nèi)層
canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, paint2);
canvas.save();
//繪制文字前平移10像素
canvas.translate(10, 0);
//父類完成方法
super.onDraw(canvas);
canvas.restore();
這個可以利用LinearGradient,Shader罚斗,Matrix徙鱼,來完成,來實現(xiàn)一個閃閃發(fā)光的閃動效果针姿,我們充分的利用Shader渲染器袱吆,來設(shè)置一個不斷變化的LinearGradient,首先我們要在onSizeChanged()方法中完成一些初始化操作
**
* 文本漸變
* Created by lgl on 16/3/4.
*/
public class CosuTwoTextView extends TextView {
int mViewWidth = 0;
private Paint mPaint;
private LinearGradient mLinearGradient;
private Matrix matrix;
private int mTranslate;
public CosuTwoTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mViewWidth == 0) {
mViewWidth = getMeasuredWidth();
if (mViewWidth > 0) {
//獲取畫筆對象
mPaint = getPaint();
//渲染器
mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{Color.BLUE, 0xffffffff, Color.BLUE},
null, Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
//矩陣
matrix = new Matrix();
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (matrix != null) {
mTranslate += mViewWidth + 5;
if (mTranslate > 2 * mViewWidth / 5) {
mTranslate = -mViewWidth;
}
matrix.setTranslate(mTranslate, 0);
mLinearGradient.setLocalMatrix(matrix);
//每隔100毫秒閃動一下
postInvalidateDelayed(100);
}
}
}
2.復合控件
創(chuàng)建一個復核人控件可以很好的創(chuàng)建出具有重要功能的控件集合距淫,這種方式經(jīng)常需要繼承一個合適的ViewGroup绞绒,再給他添加指定功能的控件,從而組成一個新的合適的控件榕暇,通過這種方式創(chuàng)建的控件蓬衡,我們一般都會給他指定的一些屬性,讓他具有更強的擴展性彤枢,下面就以一個TopBar為例子狰晚,講解如何創(chuàng)建復合控件
(1)屬性定義
我們需要給他定義一些屬性,這樣的話缴啡,我們需要在values下新建一個attrs.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TopBar">
<attr name="title" format="string" />
<attr name="titleTextSize" format="dimension" />
<attr name="titleTextColor" format="color" />
<attr name="leftTextColor" format="color" />
<attr name="leftBackground" format="reference|color" />
<attr name="leftText" format="string" />
<attr name="rightTextColor" format="color" />
<attr name="rightBackground" format="reference|color" />
<attr name="rightText" format="string" />
</declare-styleable>
</resources>
我們在代碼中是可以用< declare-styleable >標簽去聲明一些屬性的壁晒,然后name相當于ID讓我們的類可以找到,业栅,確定好之后秒咐,我們新建一個類谬晕,就叫TopBarView
public class TopBarView extends ViewGroup {
private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
private int mRightTextColor;
private Drawable mRightBackgroup;
private String mRightText;
private float mTitleSize;
private int mTitleColor;
private String mTitle;
//帶參構(gòu)造方法
public TopBarView(Context context, AttributeSet attrs) {
super(context, attrs);
//通過這個方法,你可以從你的attrs.xml文件下讀取讀取到的值存儲在你的TypedArray
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
//讀取出相應的值設(shè)置屬性
mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
mRightBackgroup = ta.getDrawable(R.styleable.TopBar_rightBackground);
mRightText = ta.getString(R.styleable.TopBar_rightText);
mTitleSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
mTitleColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
mTitle = ta.getString(R.styleable.TopBar_title);
//獲取完TypedArray的值之后携取,一般要調(diào)用recyle方法來避免重復創(chuàng)建時候的錯誤
ta.recycle();
}
(2)組合控件
實際上攒钳,這個模板也是由左右兩邊的兩個Button和中間的TextView組成的,既然這樣歹茶,我們就可以把這些屬性加給他們了,這里就可以直接定義了夕玩,完整賦值:
mLeftButton = new Button(context);
mRightButton = new Button(context);
mTitleView = new TextView(context);
//為創(chuàng)建的元素賦值
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackground(mLeftBackground);
mLeftButton.setText(mLeftText);
mRightButton.setTextColor(mRightTextColor);
mRightButton.setBackground(mRightBackgroup);
mRightButton.setText(mRightText);
mTitleView.setText(mTitle);
mTitleView.setTextColor(mTitleColor);
mTitleView.setTextSize(mTitleSize);
mTitleView.setGravity(Gravity.CENTER);
// 為組件元素設(shè)置相應的布局元素
mLeftParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
// 添加到ViewGroup
addView(mLeftButton, mLeftParams);
mRightParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
addView(mRightButton, mRightParams);
mTitlepParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(mTitleView, mTitlepParams);
// 按鈕的點擊事件,不需要具體的實現(xiàn)惊豺,
// 只需調(diào)用接口的方法燎孟,回調(diào)的時候,會有具體的實現(xiàn)
mRightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.rightClick();
}
});
mLeftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.leftClick();
}
});
(3)引用UI模板
xmlns:android="http://schemas.android.com/apk/res/android"
而我們自定義屬性的haunted尸昧,也是需要自己的命名空間
xmlns:custom="http://schemas.android.com/apk/res/custom"
<com.lgl.viewdemo.CosuTwoTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
custom:leftText="測試標題"
custom:leftBackground = "#222222"
custom:leftTextSize="18sp"/>
這樣以此類推揩页,就是我們使用自定義屬性的方法了
3.重寫View來實現(xiàn)全新的控件
重寫View主要自定義類繼承View類,分別重寫里面的onMeasure烹俗、onDraw等方法爆侣。
public class CircleView extends View {
//圓的長度
private int mCircleXY;
//屏幕高寬
private int w, h;
//圓的半徑
private float mRadius;
//圓的畫筆
private Paint mCirclePaint;
//弧線的畫筆
private Paint mArcPaint;
//文本畫筆
private Paint mTextPaint;
//需要顯示的文字
private String mShowText = "劉桂林";
//文字大小
private int mTextSize = 50;
//圓心掃描的弧度
private int mSweepAngle = 270;
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
//獲取屏幕高寬
WindowManager wm = (WindowManager) getContext()
.getSystemService(Context.WINDOW_SERVICE);
w = wm.getDefaultDisplay().getWidth();
h = wm.getDefaultDisplay().getHeight();
init();
}
private void init() {
mCircleXY = w / 2;
mRadius = (float) (w * 0.5 / 2);
mCirclePaint = new Paint();
mCirclePaint.setColor(Color.BLUE);
mArcPaint = new Paint();
//設(shè)置線寬
mArcPaint.setStrokeWidth(100);
//設(shè)置空心
mArcPaint.setStyle(Paint.Style.STROKE);
//設(shè)置顏色
mArcPaint.setColor(Color.BLUE);
mTextPaint = new Paint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(mTextSize);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪制矩形
RectF mArcRectF = new RectF((float) (w * 0.1), (float) (w * 0.1), (float) (w * 0.9), (float) (w * 0.9));
//繪制圓
canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
//繪制弧線
canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);
//繪制文本
canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + (mTextSize / 4), mTextPaint);
}
}
我們還可以設(shè)置一些其他的狀態(tài),設(shè)置他的弧度幢妄,我們寫一個對外的方法
public void setSweepValues(float sweepValues){
if(sweepValues !=- 0){
mSweepAngle = sweepValues;
}else{
//如果沒有兔仰,我們默認設(shè)置
mSweepAngle = 30;
}
//記得刷新哦
invalidate();
}
五、事件攔截機制分析
這章講的是一個事件攔截機制的一些基本概念蕉鸳,乎赴,當Android系統(tǒng)撲捉到用戶的各種輸入事件之后,如何準確的傳遞給真正需要這個事件的控件尼潮尝?其實Android提供了一套非常完善的事件傳遞榕吼,處理機制,來幫助開發(fā)者完成準確的事件分配和處理
要想了解攔截機制勉失,我們首先要知道什么事觸摸事件羹蚣,一般MotionEvent提供的手勢,我們常用的幾個DOWN,UP,MOVE什么的
在MotionEvent中封裝了很多東西乱凿,比如獲取坐標點event.getX()和getRawX()獲取
但是我們這次講的是事件攔截顽素,那什么才是事件攔截,打個比方View和ViewGroup都要這個事件徒蟆,這里就產(chǎn)生了事件攔截戈抄,其實就是一層層遞減的意義,一般ViewGroup我們需要重寫三個方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
而我們的View就只要兩個了
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
得到的點擊事件Log
從此圖可以看出后专,onInterceptTouchEvent事件的執(zhí)行順序是由父控件到子控件,并且優(yōu)先于自己控件的onTouchEvent方法執(zhí)行,onTouchEvent事件執(zhí)行的順序正好相反由子控件到父控件绊诲。注意由于此時都返回了false琴儿,是沒有哪一個view來處理此次的touch事件的各個ACTION的给梅,這也是為什么onTouchEvent為什么會一直傳遞到A的原因厅瞎。所以ACTION_MOVE和ACTION_UP等事件得不到相應(處理)又跛,此種情況下即使你在D的onTouchEvent方法里面寫了如下代碼傲武,也不會得到執(zhí)行币厕。
如果A的InterceptTouchEvent返回了true甘畅,其余的仍然返回false埂蕊,那么執(zhí)行輸出的log為:
可以發(fā)現(xiàn)此時A攔截了此次Touch事件,事件不再向A的子控件B C D傳遞疏唾。此時所有的action事件比如手指移動事件ACTION_MOVE或者ACTION_UP事件啦等等事件都交給A的onTouchEvent方法去處理(當然這是在onTouchEvent方法返回true的情況下蓄氧,如果返回false經(jīng)過測試時不會相應這些action的)。B,C ,D控件是的事件處理攔截方法和事件處理方法是無法得到執(zhí)行的槐脏。
只有B的onIntercepteTouchEvent事件返回了true的情況下喉童,打印的log為
此時由B攔截了此次Touch事件,并不會向C D子控件傳遞顿天;同樣的由于onTouchEvent事件返回為false,所以此次事件的event.getAction()的各種action都不會得到處理堂氯。
同理可知,C控件的onIntercept方法返回了true的情況下牌废,其余的仍然返回false的情況下咽白,輸出log為
下面說說各個view的onTouchEvent返回true的情況
由于onTouchEvent事件是從子控件到父控件傳遞的,當D的onTouchEvent返回true的時候鸟缕,經(jīng)測試輸出效果如下
部分圖片引用網(wǎng)上的資源晶框,如有侵權(quán)請告知。
csdn博客:http://blog.csdn.net/aaaaa_sean_m/article/details/74066896