自定义控件android.r,Android控件架构与自定义控件
前言
最近在開發的路上越走越遠了,每天在看各位大神公眾號更新內容是自定義View的時候,一些小的內容有點模具,決定回過頭來溫習一下過往的內容。此篇也是根據android群英傳來總結的一篇文章。
1 Android控件架構
Android的每個控件都是占一塊矩形的區域,大致的分兩類,繼承View和ViewGroup,ViewGroup相當于一個容器,他可以管理多個字View,整個界面上的控件形成了一個樹形結構,也就是我們常說的控件樹,上層控件負責下層控件的測量和繪制,并且傳遞交互事件,通過findviewbyid()這個方法來獲取,其實就是遍歷查找,在樹形圖的頂部都有一個ViewParent對象,這就是控制核心,所有的交互管理事件都是由它統一調度和分配,從而進行整個視圖的控制
2 View的測量
我們想要繪制一個View,首先還是得知道這個View的大小,系統是如何把他繪制出來的,在Android中,我們要想繪制一個View,就必須要知道這個View的大小,然后告訴系統,這個過程在onMeasure()中進行。
Android給我們提供了一個設計短小精悍的類——MeasureSpec類,通過他來幫助我們測量View, MeasureSpec是一個32位的int值,其中高2位為測量模式,低30為測量的大小,在計算中使用位運算時為了提高并且優化效率
三種測量模式如下:
EXACTLY 精確值模式
表示父視圖希望子視圖的大小應該是由specSize的值來決定的,系統默認會按照這個規則來設置子視圖的大小,開發人員當然也可以按照自己的意愿設置成任意的大小。
AT_MOST最大值模式
控件的尺寸不超過父控件允許的最大尺寸即可
UNSPECIFIED
表示開發人員可以將視圖按照自己的意愿設置成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。
一份模板代碼:
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;
}
3 View的繪制
Canvas顧名思義,畫布的意思,而onDraw()就一個參數,就是Canvas了,我們要在其他地方繪制的話,就需要new對象了
Canvas canvas = new Canvas(Bitmap);
//繪制直線
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.drawBitmap(Bitmap bitmap, float left, float top, Paint paint);
4 ViewGroup的測量
前面也說過,ViewGroup是老大,他是用來管理View的,包括View的大小什么的,當我們的ViewGroup大小是包裹內容的時候,實際上ViewGroup會遍歷所有的子View,來獲取View的大小,從而決定自身的大小,而在其他模式下,會通過具體的值來自定自身的大小
ViewGroup遍歷所有的View會調用所有的View的onMeasure()方法來獲取測量結果,當子View測量完畢之后,,就需要將子View放在合適的地方,這部分是由onLayout()來進行的,在我們自定義ViewGroup的時候,一般都要重寫onLayout()方法控制子View顯示位置的邏輯,同樣,如果需要wrap_content屬性,那就必須重寫onLayout()方法了,這點和View是相同的
5 ViewGroup的繪制
ViewGroup在一般情況下是不會繪制的,因為他本身沒有需要繪制的東西,如果不是指定ViewGroup的背景顏色,他連onDraw()都不會調用,但是ViewGroup會使用dispatchDraw()來繪制其他子View,其過程同樣是遍歷所喲普的子View,并調用子View的繪制方法來完成繪制的
6 自定義View
自定義View一直是個難點,Android自帶的控件很難滿足我們的需求,所欲我們需要重寫控件或者自定義一個View,但是一般強大的View,都還是存在少許的bug的,而且現在Android ROM的多樣性,適配問題也越來越麻煩了,當然,自定義View你熟悉之后,可以了解系統繪制控件的原理,而且能讓你的APP更加美觀,強大。
在View中通常有以下比較重要的回調方法
首先,我們應該了解一下比較重要的回調方法:
onDraw() 繪制View的顯示內容
onMeasure() 使用此方法時多是該View支持wrap_content屬性
onFinishInflate() 從XML加載組件后回調
onSizeChanged() 組件大小改變后回調
onMeasure(int widthMeasureSpec, int heightMeasureSpec)回調該方法進行測量
onLayout(boolean changed, int left, int top, int right, int bottom)回調該方法確定顯示位置
onTouchEvent(MotionEvent event) 監聽到觸摸事件時的回調
以上就是幾種常用的回調的方法.上面的方法并不需要全部寫出來,看個人需要,一般我們實現自定義控件有三種方法
對現有的控件進行擴展
通過組件來實現新的控件
重寫View來實現全新的控件
6.1 對現有控件進行擴展
這是一個我們十分常用的一個方法,用來對現有的控件進行擴展,比如TextView需要漸變啊什么的,挺常用的,這里我們就來寫一個小栗子,我們先來看下效果
public class MyTextView extends TextView {
private Paint mPaint1, mPaint2;
public MyTextView(Context context) {
super(context);
initView();
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MyTextView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mPaint1 = new Paint();
mPaint1.setColor(getResources().getColor(
android.R.color.holo_blue_light));
mPaint1.setStyle(Paint.Style.FILL);
mPaint2 = new Paint();
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制外層矩形
canvas.drawRect(
0,
0,
getMeasuredWidth(),
getMeasuredHeight(),
mPaint1);
// 繪制內層矩形
canvas.drawRect(
10,
10,
getMeasuredWidth() - 10,
getMeasuredHeight() - 10,
mPaint2);
canvas.save();
// 繪制文字前平移10像素
canvas.translate(10, 0);
// 父類完成的方法,即繪制文本
super.onDraw(canvas);
canvas.restore();
}}
一個稍微復雜的TextView:
這個可以利用LinearGradient,Shader,Matrix,來完成,來實現一個閃閃發光的閃動效果,我們充分的利用Shader渲染器,來設置一個不斷變化的LinearGradient,首先我們要在onSizeChanged()方法中完成一些初始化操作
public class CoolTextView extends TextView {
private int mViewWidth;
//初始化畫筆
private Paint mPaint;
//渲染器
private LinearGradient mLinearGradient;
//矩陣
private Matrix matrix;
private int mTranslate;
public CoolTextView(Context context) {
super(context);
}
public CoolTextView(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);
mPaint=new Paint();
if (mViewWidth==0){
mViewWidth=getMeasuredWidth();
if (mViewWidth>0){
//獲取當前TextView的畫筆
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/10;
if (mTranslate>1*mViewWidth){
mTranslate=-mViewWidth;
}
matrix.setTranslate(mTranslate,0);
mLinearGradient.setLocalMatrix(matrix);
//每隔100毫秒閃動一下
postInvalidateDelayed(100);
}
}
/*
LinearGradient參數:
float x0: 漸變起始點x坐標
float y0:漸變起始點y坐標
float x1:漸變結束點x坐標
float y1:漸變結束點y坐標
int[] colors:顏色 的int 數組
float[] positions: 相對位置的顏色數組,可為null, 若為null,可為null,顏色沿漸變線均勻分布
Shader.TileMode tile: 渲染器平鋪模式*/
}
**6.2 復合控件 **
創建一個復核人控件可以很好的創建出具有重要功能的控件集合,這種方式經常需要繼承一個合適的ViewGroup,再給他添加指定功能的控件,從而組成一個新的合適的控件,通過這種方式創建的控件,我們一般都會給他指定的一些屬性,讓他具有更強的擴展性,下面就以一個TopBar為例子,講解如何創建復合控件
定義屬性
我們需要給他定義一些屬性,這樣的話,我們需要在values下新建一個attrs.xml文件
我們在代碼中是可以用< declare-styleable >標簽去聲明一些屬性的,然后name相當于ID讓我們的類可以找到,,確定好之后,我們新建一個類,就叫TopBarView
package com.zc.demo;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.ViewGroup;
/**
* TopBar
* Created by Zc on 17/8/31.
*/
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;
//帶參構造方法
public TopBarView(Context context, AttributeSet attrs) {
super(context, attrs);
//通過這個方法,你可以從你的attrs.xml文件下讀取讀取到的值存儲在你的TypedArray
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
//讀取出相應的值設置屬性
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的值之后,一般要調用recyle方法來避免重復創建時候的錯誤
ta.recycle();
}
6.3 重寫View來實現全新的控件
當我們Android原生的控件不滿足的話,我們可以繼承原來的控件修改,也可以組合起來使用,更加可以繼承View創建一個新的控件View
效果如圖所示
public class CircleProgressView extends View {
private int mMeasureHeigth;
private int mMeasureWidth;
private Paint mCirclePaint;
private float mCircleXY;
private float mRadius;
private Paint mArcPaint;
private RectF mArcRectF;
private float mSweepAngle;
private float mSweepValue = 66;
private Paint mTextPaint;
private String mShowText;
private float mShowTextSize;
public CircleProgressView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CircleProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CircleProgressView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
mMeasureWidth = MeasureSpec.getSize(widthMeasureSpec);
mMeasureHeigth = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(mMeasureWidth, mMeasureHeigth);
initView();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制圓
canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
// 繪制弧線
canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);
// 繪制文字
canvas.drawText(mShowText, 0, mShowText.length(),
mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint);
}
private void initView() {
float length = 0;
if (mMeasureHeigth >= mMeasureWidth) {
length = mMeasureWidth;
} else {
length = mMeasureHeigth;
}
mCircleXY = length / 2;
mRadius = (float) (length * 0.5 / 2);
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(true);
mCirclePaint.setColor(getResources().getColor(
android.R.color.holo_blue_bright));
mArcRectF = new RectF(
(float) (length * 0.1),
(float) (length * 0.1),
(float) (length * 0.9),
(float) (length * 0.9));
mSweepAngle = (mSweepValue / 100f) * 360f;
mArcPaint = new Paint();
mArcPaint.setAntiAlias(true);
mArcPaint.setColor(getResources().getColor(
android.R.color.holo_blue_bright));
mArcPaint.setStrokeWidth((float) (length * 0.1));
mArcPaint.setStyle(Style.STROKE);
mShowText = setShowText();
mShowTextSize = setShowTextSize();
mTextPaint = new Paint();
mTextPaint.setTextSize(mShowTextSize);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
private float setShowTextSize() {
this.invalidate();
return 50;
}
private String setShowText() {
this.invalidate();
return "Panda_Program";
}
public void forceInvalidate() {
this.invalidate();
}
public void setSweepValue(float sweepValue) {
if (sweepValue != 0) {
mSweepValue = sweepValue;
} else {
mSweepValue = 25;
}
this.invalidate();
}
}
在來一個例子 效果如下所示
模擬音頻輸入
public class VolumeView extends View {
private int mWidth;
private int mRectWidth;
private int mRectHeight;
private Paint mPaint;
private int mRectCount;
private int offset = 5;
private double mRandom;
private LinearGradient mLinearGradient;
public VolumeView(Context context) {
super(context);
initView();
}
public VolumeView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public VolumeView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mPaint = new Paint();
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL);
mRectCount = 12; //條形數量
}
//條形的漸變色
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getWidth();
mRectHeight = getHeight();
mRectWidth = (int) (mWidth * 0.6 / mRectCount);
mLinearGradient = new LinearGradient(
0,
0,
mRectWidth,
mRectHeight,
Color.YELLOW,
Color.BLUE,
Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mRectCount; i++) {
mRandom = Math.random();
float currentHeight = (float) (mRectHeight * mRandom);
canvas.drawRect(
(float) (mWidth * 0.4 / 2 + mRectWidth * i + offset),
currentHeight,
(float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)),
mRectHeight,
mPaint);
}
//300毫秒重新繪制一次
postInvalidateDelayed(300);
}
}
7 自定義ViewGroup
這個管理子View的管理者,我們來定義一下,通常我們自定義ViewGroup是需要onMeasure()來測量的,然后重寫onLayout()來確定位置,重寫onTouchEvent()來相應事件
接下來制作一個仿ScrollView的效果并且增加粘性事件。
public class MyScrollView extends ViewGroup {
private int mScreenHeight;
private Scroller mScroller;
private int mLastY;
private int mStart;
private int mEnd;
public MyScrollView(Context context) {
super(context);
initView(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public MyScrollView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(
Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
mScreenHeight = dm.heightPixels;
mScroller = new Scroller(context);
}
@Override
protected void onLayout(boolean changed,
int l, int t, int r, int b) {
int childCount = getChildCount();
// 設置ViewGroup的高度
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.height = mScreenHeight * childCount;
setLayoutParams(mlp);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
child.layout(l, i * mScreenHeight,
r, (i + 1) * mScreenHeight);
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; ++i) {
View childView = getChildAt(i);
measureChild(childView,
widthMeasureSpec, heightMeasureSpec);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
int dy = mLastY - y;
if (getScrollY() < 0) {
dy = 0;
}
if (getScrollY() > getHeight() - mScreenHeight) {
dy = 0;
}
scrollBy(0, dy);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
int dScrollY = checkAlignment();
if (dScrollY > 0) {
if (dScrollY < mScreenHeight / 3) {
mScroller.startScroll(
0, getScrollY(),
0, -dScrollY);
} else {
mScroller.startScroll(
0, getScrollY(),
0, mScreenHeight - dScrollY);
}
} else {
if (-dScrollY < mScreenHeight / 3) {
mScroller.startScroll(
0, getScrollY(),
0, -dScrollY);
} else {
mScroller.startScroll(
0, getScrollY(),
0, -mScreenHeight - dScrollY);
}
}
break;
}
postInvalidate();
return true;
}
private int checkAlignment() {
int mEnd = getScrollY();
boolean isUp = ((mEnd - mStart) > 0) ? true : false;
int lastPrev = mEnd % mScreenHeight;
int lastNext = mScreenHeight - lastPrev;
if (isUp) {
//向上的
return lastPrev;
} else {
return -lastNext;
}
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
}
8 事件攔截機制分析
這章講的是一個事件攔截機制的一些基本概念,,當Android系統撲捉到用戶的各種輸入事件之后,如何準確的傳遞給真正需要這個事件的控件尼?其實Android提供了一套非常完善的事件傳遞,處理機制,來幫助開發者完成準確的事件分配和處理要想了解攔截機制,我們首先要知道什么事觸摸事件,一般MotionEvent提供的手勢,我們常用的幾個DOWN,UP,MOVE什么的在MotionEvent中封裝了很多東西,比如獲取坐標點event.getX()和getRawX()獲取
一般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);
}
小結
至此空間架構與自定義控件基礎內容就完成了,日后碰見有趣的自定義控件,我會更新在我的博客上,歡迎瀏覽
總結
以上是生活随笔為你收集整理的自定义控件android.r,Android控件架构与自定义控件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 统计了50万人的身高数据才发现:大家都爱
- 下一篇: 铔潼字的女孩名字寓意?