一个设计良好的自定义视图非常类似于其他任何精心设计的类。
它封装了一组特定的功能和一个易于使用的界面,它高效使用CPU和内存,等等。除了是一个精心设计的类,一个定制的视图应该:
符合Android标准
提供定制styleable属性使用Android XML布局
发送访问事件
与多个Android平台兼容。
一、创建自定义view的java类
1.创建一个自定义View,AView继承View或者View的子类,比如Button。如果你要自定义一个按钮那么比较好的做法就是继承一个Button。
2.如果我们希望和系统的Button一样可以通过xml来设置(如果不需要这一步可能省略)。
2.1 Aview中要提供至少一个带Context和AttributeSet参数的构造方法(在xml中定义的属性,比如text=”test“,系统会读取xml并把数据加载到AttributeSet中)
class PieChart extends View { public PieChart(Context context, AttributeSet attrs) { super(context, attrs); } }2.2 按照如下代码编写构造方法,虽然AttributeSet中可以直接读取配置的值,但是Style与应用资源的属性值没有解决(比如属性值的类型等)。
public PieChart(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.PieChart, 0, 0); try { mShowText = a.getBoolean(R.styleable.PieChart_showText, false); mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0); } finally { a.recycle(); } }3. 提供View属性的get,set方法比如:
public boolean isShowText() { return mShowText; } public void setShowText(boolean showText) { mShowText = showText; invalidate(); requestLayout(); }Tips:
(特别注意,当set方法中改变的属性需要刷新View的话,比如形状改变,文字改变等,一定要在变化后调用 invalidate(); requestLayout();通知系统重绘View,如果忘记了,就会导致一些很难定位的Bug)
4.提供View的事件。比如
protected void onSelectionChanged(int selStart, int selEnd) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
}
Tips:
当使用自定义View的时候是非常容易遗漏事件的,因为你当时可能只用到了几个事件,而后期你需要处理新事件时,就如要维护这个定制View,增加新的事件。一个好的做法就是抽出一点时间,把可能要用的事件,尽可能多的提供出来。
二、定制View的图形
要实现定制View有许多方法可以重写,这里提供几个最常用,最基本的绘图相关方法;
1.重写onDraw()方法
画一个定制的视图中最重要的一步是重写onDraw()方法。onDraw()的一个参数Canvas是一个画布对象,可以用来画View本身。
画布上可以绘制文本,线,位图,和许多其他的基本图形。您可以使用onDraw()提供的方法来创建您的自定义用户界面(UI)。
在绘画View之前,我们还需要一支笔Paitn()方法。后面会有详细的解释。
2.创建绘图对象
Canvas:画什么
Paint:怎么画。
比如画一个矩形(使用Canvas),矩形的线条颜色,是否用颜色填满该矩形(使用Paint)。创建方式如下:
private void init() { mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(mTextColor); if (mTextHeight == 0) { mTextHeight = mTextPaint.getTextSize(); } else { mTextPaint.setTextSize(mTextHeight); } mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPiePaint.setStyle(Paint.Style.FILL); mPiePaint.setTextSize(mTextHeight); mShadowPaint = new Paint(0); mShadowPaint.setColor(0xff101010); mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL)); ...初始canvas与Paint不要放在再onDraw()方法中,因为onDraw()方法在View改变时,会多次被回调。每一次都初始化一次,显然会多创建很多的对象,多费时间,这会影响View重绘的速度,从而影响用户体验。
三、处理布局事件
如何绘制view已经在上面说了,那么View的可绘画区域的大小是多少呢?这不光和view本身有关,还和View所在布局有关。比如横屏与竖屏时。一些负责的View中还包含了其他的View。这样如何来计算View的大小呢。有很多的方法,但通常只需要重写onSizeChanged()
和onMeasure()方法,其他用默认的就行。
onSizeChanged()方法在View第一次分配到size的时候和之后任何原因引起的大小改变,都会回调这个方法。这个size是包括了View的Padding,因此需要计算view中图形的大小还要计算Padding的大小。比如:
// Account for padding float xpad = (float)(getPaddingLeft() + getPaddingRight()); float ypad = (float)(getPaddingTop() + getPaddingBottom()); // Account for the label if (mShowText) xpad += mTextWidth; float ww = (float)w - xpad; float hh = (float)h - ypad; // Figure out how big we can make the pie. float diameter = Math.min(ww, hh);onMeasure()方法的一个参数
View.MeasureSpec
会告诉你View所在的父View对你View的要求,比如是否给了一个硬性的大小,或者是建议你尽可能的占满空间。这些要求都是通过 View.MeasureSpec
返回。用法如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Try for a width based on our minimum int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); int w = resolveSizeAndState(minw, widthMeasureSpec, 1); // Whatever the width ends up being, ask for a height that would let the pie // get as big as it can int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop(); int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0); setMeasuredDimension(w, h); }Tips:
1.覆写onMeasure方法的时候,子类有责任确保measured height and width至少为这个View的最小height和width。
2. 有一个约定:在覆写onMeasure方法的时候,必须调用 setMeasuredDimension(int,int)来存储这个View经过测量得到的measured width and height。如果没有这么做,将会由measure(int, int)方法抛出一个运行时异常。
实际如何绘制图形呢,以下给了一些参考代码:
protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw the shadow canvas.drawOval( mShadowBounds, mShadowPaint ); // Draw the label text canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint); // Draw the pie slices for (int i = 0; i < mData.size(); ++i) { Item it = mData.get(i); mPiePaint.setShader(it.mShader); canvas.drawArc(mBounds, 360 - it.mEndAngle, it.mEndAngle - it.mStartAngle, true, mPiePaint); } // Draw the pointer canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint); canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint); }四、View与用户的交互
以上我们绘制了view,也让view显示出来了。但看见的同时,往往还需要响应一些用户的操作。即交互。
如何定义这些交互呢。通常就是尽可能的贴近现实,比如图片应该是从在移动到那,而不是从这消失,从那突然出现。
这里我们采用系统框架提供的事件(单击,长按等),能满足巨大多数的要求。其中复杂一点就是手势事件,比如用户滑动手指。
4.1 处理手势事件
在View上用户进行操作时都会触发View中的onTouchEvent(android.view.MotionEvent)
.其中的MotionEvent中包含了用户操作的所有信息,比如用户在屏幕什么位置点击了,滑动到哪里了。直接用这些数据比较麻烦(比如根据这些数据判断用户滑动了没有),Android提供了GestureDetector
.手势检测类,使用这个类需要实现一个监听GestureDetector.OnGestureListener。如果不需要那么多手势的话可以使用GestureDetector.SimpleOnGestureListener。实例代码:
class mListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(MotionEvent e) { return true; } } mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());Tips:
public boolean onDown(MotionEvent e) 返回true就代表事件已经处理完毕,不用再处理了。把MotionEvent传给GestureDetector,GestureDetector会自动调用对应的手势事件。如果不能识别的话,就会返回false;示例代码:
@Override public boolean onTouchEvent(MotionEvent event) { boolean result = mDetector.onTouchEvent(event); if (!result) { if (event.getAction() == MotionEvent.ACTION_UP) { stopScrolling(); result = true; } } return result; }4.2 手势命令高级技巧
要贴近人类的思维。比如滑动时,开始慢慢加速,到最后减速,就想物理中的飞轮一样。如果把MotionEvent变为飞轮来处理。是需要数学模型与复杂的计算。当然Android中可以直接使用Scroller
这个帮助类来完成。设置初始速度,每次最少移动和最多移动。其他的比如飞轮效果,由Scroller来完成就好。可惜Scroller只会帮你计算出某一时刻,应该要运动到位置X和Y。所以为什么看起来连贯,你需要周期行的调用 Scroller.computeScrollOffset(),
getCurrX(),
getCurrY()
来获取当前的位置X和Y。然后通过 scrollTo()方法让view真正移动起来。但是View的移动还是很生硬,比如从X=1到X=5是突然消失,突然出现的。因此为了顺滑,通常使用
ValueAnimator(在3.0以后才提供)。
@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mScroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY); postInvalidate(); }4.3 让view更顺滑
Android 3.0可以使用Android property animation framework,属性动画框架,完成。
五、优化View
5.1 在经常被回调的方法中,尽可能的简化代码,不要在回调方法中不断的创建对象,要放到初始化方法中。经常被回调的方法有:
onDraw(),动画进行过程中的一些方法。
5.2 尽可能的减少view的层次,因为测量View的方法也会被经常调用,会从上往下调用,这样如果层次很多,或者view测量中view之间有冲突,会照成测量多次调用,极大的影响了性能。
5.3 android 3.0以上支持GPU硬解加速,在位移,放大缩小,翻转图像时有极大的性能提升,但比如画线条,曲线时就没有明显的效果。