Android自定义View

一个设计良好的自定义视图非常类似于其他任何精心设计的类。

它封装了一组特定的功能和一个易于使用的界面,它高效使用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硬解加速,在位移,放大缩小,翻转图像时有极大的性能提升,但比如画线条,曲线时就没有明显的效果。





发布了32 篇原创文章 · 获赞 1 · 访问量 5万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章