这章的重要性不言而喻,按照以下几个方面进行介绍:
- Android控件架构
- View的测量与绘制
- 自定义控件的三种方式
- 事件的拦截机制
3.1 Android控件架构
3.2 View的测量
Android系统提供了一个设计短小精悍的类---MeasureSpec类,通过它帮助我们测量。MeasureSpec是一个32位的int值,其中高2位是测量的模式,低30位是测量的大小。
注意:View类默认的onMeasure()方法只支持EXACTLY模式。所以在自定义控件的时候不重写onMeasure的话,就只能使用EXACTLY模式。控件可以响应你指定的具体的宽高或match_parent属性。而如果要让自定义view支持wrap_content属性,那么就要重写onMeasure。
package com.fightzhao.gesturedetectordemo.ui; import android.content.Context; import android.util.AttributeSet; import android.view.View; /** * Created by fightzhao on 16-3-7. */ public class TestView extends View { public TestView(Context context) { super(context); } public TestView(Context context, AttributeSet attrs) { super(context, attrs); } public TestView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHight(heightMeasureSpec)); } 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; } private int measureHight(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.3 View的绘制
这个书中写的不是太明白,难点在于创建Canvas时候为何在其构造函数中传入的参数是bitmap。
Canvas相当于是一个画板,使用Paint在上面作画了。通常是用过继承view,重写其中的onDraw()方法来完成绘图。
那么什么是Canvas呢?一般情况下,可以使用重写的View类中的onDraw方法来绘图。onDraw中有一个参数就是Canvas canvas对象。使用这个对象就可以进行绘图了,而在其他地方,通常需要代码创建一个Canvas对象:
Canvas canvas = new Canvas(bitmap);
当创建一个Canvas对象的时候,为什么要传进去一个bitmap对象呢?如果不传入一个bitmap的话,编译器虽然不会报错,,但是一般我是不会这么做的。原因如下:
- 传进去的bitmap和与这个bitmap创建的canvas是紧密联系在一起的,这个过程我们称之为装载画布。这个bitmap用来存储所有绘制在Canvas上的像素信息。所以通过这种方式创建的canvas对象后,后面调用所有的Canvas.drawXXX方法都发生在这个bitmap上。
- 如果在view类的onDraw()方法,通过如下代码:可以了解canvas和bitmap的关系:
canvas.drawBitmap(bitmap1,0,0,null);
canvas.drawBitmap(bitmap2,0,0,null);
而对于bitmap2,我们将它装载到另一个Canvas对象中: Canvas mCanvas = new Canvas(bitmap2);
在其他地方使用Canvas对象的绘图方法在装载bitmap2的Canvas对象进行绘图: mCanvas.drawXXX
通过mCanvas将绘制效果作用在了bitmap2上,再刷新View的时候,就会发现通过onDraw()方法画出来的bitmap2已经发生了改变,这就是因为bitmap2承载了mCanvas上所进行的绘图操作。虽然我们也使用了Canva的绘制API,但是实际上并没有直接将图形绘制在onDraw()方法制定的画布上,而是通过改变bitmap,然后让view进行重新绘制,从而显示改变后的bitmap。
3.6 自定义View
- 可以分为以下三类:
- 对现有的控件进行拓展,需要重写onDraw()比如TextView。那么什么时候使用呢?这种效果不方便通过布局的组合方式展现的时候,往往需要动态或者静态的展现一些不规则图形的时候,即需要重写onDraw().。采用这种方式需要自己手动的自己支持wrap_content,并且可以padding。
- 通过组合来实现新的控件:
- 继承ViewGroup派生特殊的Layout:这种方法用在需要自定义布局的时候,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统的布局外,我们重新定义一种新的布局的时候,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法实现。采用这种方法要麻烦一点,需要合适的处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局。
- 继承特定的ViewGroup(比如LinearLayout):这种方法比较常见,当效果看起来像是几个View组合在一起的效果的时候,可以用这种方法实现。不需要自己处理ViewGroup的测量和布局,注意和上面的区别,一般来说上面能实现的,这个也一定可以实现,两者差别在于上面的更加接近View的底层。、
- 重写View,实现全新的效果。
依次介绍这几种重写view的方法:
- 第一种是对现有View进行拓展:
package com.fightzhao.gesturedetectordemo.ui; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.widget.TextView; /** * Created by fightzhao on 16-3-8. */ public class MyTextView extends TextView { private Paint mPaint1; private Paint mPaint2; public MyTextView(Context context) { super(context); init(); } public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mPaint1 = new Paint(); mPaint1.setColor(Color.BLUE); mPaint1.setStyle(Paint.Style.FILL); mPaint1.setStrokeWidth(2); mPaint2 = new Paint(); mPaint2.setColor(Color.YELLOW); mPaint2.setStyle(Paint.Style.FILL); } @Override protected void onDraw(Canvas canvas) { // 在回调父类方法前,实现自己的逻辑,对TextView来说就是绘制文本之前 // 绘制外层矩形 int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingLeft(); int paddingTop = getPaddingLeft(); int paddingBoom = getPaddingLeft(); int width = getWidth() - paddingLeft - paddingRight; int height = getHeight() - paddingTop - paddingBoom; canvas.drawRect(0, 0, getMeasuredWidth(), getHeight(), mPaint1); // 绘制内层圆 int radius = Math.min(width, height) / 2; canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint2); canvas.save(); canvas.translate(10, 0); super.onDraw(canvas); // 在回调父类方法后,实现自己的逻辑,对TextView来说就是绘制文本内容之后 canvas.restore(); } }
再来实现一个稍微复杂的:预览图如下:
具体代码:
package com.fightzhao.gesturedetectordemo.ui; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Shader; import android.util.AttributeSet; import android.widget.TextView; /** * Created by fightzhao on 16-3-7. */ public class TestView extends TextView { private int mViewWidth,mTranslate; private Paint mPaint; private LinearGradient mLinearGradient; private Matrix mMatrix; public TestView(Context context) { super(context); } public TestView(Context context, AttributeSet attrs) { super(context, attrs); } public TestView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mPaint = new Paint(); mPaint.setColor(Color.YELLOW); mPaint.setStyle(Paint.Style.FILL); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHight(heightMeasureSpec)); } 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; } private int measureHight(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) { result = specSize; } else { result = 150; if (specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } } return result; } @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,0xfffffff,Color.BLUE },null, Shader.TileMode.CLAMP); mPaint.setShader(mLinearGradient); mMatrix = new Matrix(); } } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(mMatrix!=null){ mTranslate += mViewWidth / 5; if (mTranslate > 2 * mViewWidth) { mTranslate = -mViewWidth; } mMatrix.setTranslate(mTranslate, 0); mLinearGradient.setLocalMatrix(mMatrix); postInvalidateDelayed(100); } } }
这里有两个要点:一个是设置的wrap_content的实现。需要重写onMeasure()方法。
第二个:设置的效果直接看书。