Android自己定义View

书到用时方恨少,事非经过不知难。这篇文章主要讲述Android自己定义View相关的知识,希望能为你提供帮助。
翻译自:http://developer.android.com/training/custom-views/index.html
一)创建view类一个设计良好的自己定义view与其它的类一样。它使用接口来封装一系列的功能。有效的使用CPU和内存等。除了这些,定制view还应该满足例如以下条件:

  • 符合Android标准
  • 与Android XML 布局文件配合,提供符合style风格的定制属性
  • 发送易接近性事件(accessibility events,针对听力或视觉有缺陷用户提供方便的事件)
  • 兼容不同的Android平台


Android框架提供了一系列的基类和XML标识以便用户能够非常方便的创建满足上述条件的View。
该节将讨论怎样使用Android框架来创建view类的核心功能。
1)基于View创建子类
Android框架中的全部view类都继承自View类。自己定义view也能够直接继承View类,或它的已经定义的子类。如Button类。
为了与Android studio交互,至少要定义一个以Context和AttributeSet对象为參数的构造函数,该构造函数执行布局编辑器创建和改动view的实例,如:
classPieChartextendsView{
    public PieChart(Context context, AttributeSet attrs) {         super(context, attrs);     } }

2)加入自己定义属性
我们能够通过在布局文件里加入XML元素来将内置的View类加入到用户界面, 而且能够改动元素属性来控制它的样式和行为。
设计良好的自己定义view也应该能够通过XML元素来加入和改动style. 通过下面步骤来启动自己定义view的上述行为:

  • 在< declare-styleable> 资源属性中加入view的自己定义属性
  • 在XML布局中指定属性的值
  • 执行时获取属性值
  • 将上述步骤中获取的属性值应用到view中

【Android自己定义View】通常情况下,在res/values/attrs.xml文件里加入< declare-styleable> ,如:
< resources>     < declare-styleable name="PieChart">         < attr name="showText" format="boolean" />         < attr name="labelPosition" format="enum">             < enum name="left" value="https://www.songbingjia.com/android/0"/>             < enum name="right" value="https://www.songbingjia.com/android/1"/>         < /attr>     < /declare-styleable> < /resources>

声明了两个自己定义属性:showText和labelPosition,归属于PieChart样式,依照惯例,样式名字一般都取与view类同样的名字,很多代码编辑器依赖这个来提供语句补充功能。
对上述自己定义属性。我们能够像使用内置属性一样在布局文件里使用,唯一的差别是他们属于不同的命名空间。
它们属于http://schemas.android.com/apk/res/[yourpackage name]. 而内置属性属于http://schemas.android.com/apk/res/android  命名空间。
它们的使用例如以下:
< ?
xml version="1.0" encoding="utf-8"?>
< LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">   < com.example.customviews.charting.PieChart       custom:showText="true"       custom:labelPosition="left" /> < /LinearLayout>

上例中使用xmlns命令来避免反复非常长的命名空间URI,该命令给http://schemas.android.com/apk/res/com.example.customviews指定了一个别名custom。另外,在布局文件里加入XML标示时,须要使用view类的全名。
假设自己定义的view类时一个内部类的话。必现包括它的外部类的名字。如要使用PieChart  类的内部类  PieView  的话。使用标识:  com.example.customviews.charting.PieChart$PieView.
3)使自己定义属性生效

当在XML文件里创建view时。XML标识上的全部属性被从资源bundle中读入并作为一个AttributeSet实例传输到view的构造函数中。虽然能够直接从AttributSet实例中读取属性值,由于例如以下原因不被推荐:

  • 属性值中的资源引用没有指向对应的值
  • 样式还没有没应用

因此,须要使用  obtainStyledAttributes()  来处理AttributeSet。它会返回一个TypedArray。包括已经解引用和採用样式的属性数组。
Android 资源编译器做了非常多工作以便我们能够方便的调用  obtainStyledAttributes()  。 对res目录下的每个  < declare-styleable>   资源, 生成的R.java文件定义了一组常量来指向每个属性。能够使用这些常量从  TypedArray中读取属性值,如:
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();     } }

须要注意的是。TypedArray  对象是一个共享的资源,使用后必须回收。
官网关于obtainStyledAttributes的定义:

public  TypedArray  obtainStyledAttributes  (AttributeSet  set, int[] attrs, int defStyleAttr, int defStyleRes)Added in  API level 1Return a TypedArray holding the attribute values in  set  that are listed in  attrs. In addition, if the given AttributeSet specifies a style class (through the "style" attribute), that style will be applied on top of the base attributes it defines.
Be sure to call  TypedArray.recycle()  when you are done with the array.
When determining the final value of a particular attribute, there are four inputs that come into play:
  1. Any attribute values in the given AttributeSet.
  2. The style resource specified in the AttributeSet (named "style").
  3. The default style specified by  defStyleAttr  and  defStyleRes
  4. The base values in this theme.
Each of these inputs is considered in-order, with the first listed taking precedence over the following ones. In other words, if in the AttributeSet you have supplied  < Button textColor="#ff000000"> , then the button‘s text will  always  be black, regardless of what is specified in any of the styles.
Parameters
set AttributeSet: The base set of attribute values. May be null.
attrs int: The desired attributes to be retrieved.
defStyleAttr int: An attribute in the current theme that contains a reference to a style resource that supplies defaults values for the TypedArray. Can be 0 to not look for defaults.
defStyleRes int: A resource identifier of a style resource that supplies default values for the TypedArray, used only if defStyleAttr is 0 or can not be found in the theme. Can be 0 to not look for defaults.


4)加入属性和事件
通过属性能够控制view的行为和样式。但确定是仅仅在view初始化时被读取。提供每一个自己定义属性的getter和setter方法能够动态的改动行为,如:publicboolean isShowText(){
    return mShowText; }public void setShowText(boolean showText) {     mShowText = showText;     invalidate();     requestLayout(); }

注意setShowText  调用了  invalidate()  和  requestLayout(). 这些调用对view的行为非常重要。改动不论什么可能影响view外观的属性时,必须使view失效,以便系统知道它须要又一次绘制。相同的,改动不论什么可能会引起view尺寸大小的属性时,必须请求又一次布局。遗漏了上述方法会引起非常难定位的bug.
自己定义view应该支持事件监听器以便于重要事件的交互。如,  PieChart 提供了一个自己定义事件  OnCurrentItemChanged  通知监听者用户对饼图做了改动。
非常easy忘记将属性和事件公开以便使用者使用。
一个非常好的原则是:总是公开会影响定制view可见外观和行为的属性。
5)设计支持易接近性
能够通过下面方法为听力或视觉有缺陷用户提供方便:
  • 使用  android:contentDescription  属性给输入区域做标签
  • 在须要时通过调用sendAccessibilityEvent()  发送易接近性事件
  • 支持触屏以外的其它控制器。如D-pad 和traceball

很多其它信息。见Android 开发文档:Making ApplicationsAccessible  .

二)自己定义绘画
1)重写onDraw()
onDraw()  使用Canvas对象作为參数,Canvas  类定义了绘制文字,线条,位图及其它简单图形的函数。能够在  onDraw()  中使用这些方法创建定制的用户界面。


2)创建绘画对象
框架将绘画分为两个部分:

  • 画什么。由  Canvas  类来负责
  • 怎么画,由Paint  类来负责

比如, Canvas类提供了绘制线条的方法,而Paint类提供了定义线条颜色的方法。 Canvas类有绘制长方形的方法, 而Paint类定义是否用颜色填充长方形或让它为空。
总之,Canvas类定义能够在屏幕上绘制的形状,而Paint类为绘制的形状定义颜色,字体等。
因此,在绘制之前,须要创建一个或多个Paint对象。
以下的PieChart类在构造函数中调用init方法,该方法用来创建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)); ...

提前创建绘画对象是一个非常重要的优化。页面常常会被又一次绘制,会产生非常多绘画对象。致使初始化成本变得非常高。
在onDraw()方法中创建绘画对象会非常明显地影响性能,而且引起UI迟滞。
3)处理布局
在绘制定制view前,须要知道它的尺寸。复杂的定制view须要依据屏幕上绘制区域的尺寸和形状运行多次布局计算。永远都不应该对view在屏幕上的尺寸做如果。即使仅仅有一个app使用你的view,该app也须要处理不同的屏幕尺寸,不同的屏幕密度和不同的纵横屏幕比例。
虽然View  类有非常多处理尺寸的方法。 大多数不须要被重写。假设定制页面须要对尺寸做特殊的处理,仅仅须要又一次一个方法:onSizeChanged().
onSizeChanged()  会在view页面首次被指定一个尺寸和view页面尺寸改变时被调用。在onSizeChanged()方法中计算位置。尺寸,而不是在每次绘制时又一次计算。
  PieChart  中,在onSizeChanged()  中计算包括饼图的正方形的尺寸及文字图标和其它可见元素的相对位置。
当view被指定一个尺寸后,布局管理器如果该尺寸已经包括view的填充(即padding)。
因此。计算view尺寸时必须处理填充值。以下代码节选自PieChart.onSizeChanged(),显示怎样来做:
        // 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.MeasureSpec  去解开存储在每一个整数中的信息。

下面是onMeasure()的实现,PieChart试图使它的饼图尽可能的大:
@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); }

上述代码有三点须要注意:

  1. 计算时须要考虑填充(padding)的问题
  2. 辅助函数resolveSizeAndState()被用来生成终于的宽度和高度值,该函数通过比較view请求的尺寸和onMeasure()方法传入的尺寸要求。返回一个合适的View.MeasureSpec  值
  3.   onMeasure()  没有返回值,它通过调  setMeasuredDimension()  方法将结果传送出去。假设忽略该方法的话。View类会抛出异常

4)開始绘制
创建绘画对象和处理好布局后,就能够实现  onDraw()方法了。例如以下是一些经常使用的操作:
  • 使用drawText()绘制文字。通过  setTypeface()指定字体(typeface),通过setColor()指定文字颜色
  • 使用  drawRect(),  drawOval(), 和drawArc()绘制基础形状,通过  setStyle()设置是否被填充,是否显示轮廓。

  • 使用Path  类来绘制负责的形状。 加入直线和曲线到Path  对象定义形状,然后使用  drawPath() 绘制该形状。也能够通过  setStyle()设置paths是否被填充。是否显示轮廓
  • 通过创建  LinearGradient  对象来定义渐变填充, 调用  setShader()  将该LinearGradient对象填充到须要绘制的形状中。

  • 使用drawBitmap()绘制位图。


示比例如以下:
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加入交互
绘制UI仅仅是创建定制view的一部分。view还须要响应用户输入。
设计交互时,须要注意模仿现实世界的操作。比如,图片不应该在一个地方马上退出然后又在另外一个地方又一次出现,而应该从一个地方移动到另外一个地方。
这一节展示怎样使用Android框架的特性来给定值view加入真实世界的行为。

1)处理输入手势
同其它UI框架一样,Android支持输入数据模型。用户操作被转换成引起回调的事件。这些回调能够被重写以便定制应用程序对用户操作的回应。
Android系统中最常见的输入事件是触摸事件(touch), 它会引起onTouchEvent(android.view.MotionEvent).回调。
重写该函数来处理事件:
    @Override     public boolean onTouchEvent(MotionEvent event) {     return super.onTouchEvent(event);     }

触摸事件本身不是特别实用。
现代的UI系统会依据手势将其分类,如点击(tapping), 上拉或下拉(pulling), pushing,急冲(flinging),缩放(zooming)等。Android提供GestureDetector类来将原始的触摸事件转换为手势。
以一个实现GestureDetector.OnGestureListener接口的类实例为參数来构造GestureDetector类。假设仅仅须要处理简单的手势,能够继承  GestureDetector.SimpleOnGestureListener  类而不须要实现GestureDetector.OnGestureListener接口。
例如以下例:
class mListener extends GestureDetector.SimpleOnGestureListener {     @Override     public boolean onDown(MotionEvent e) {         return true;     } } mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());

不管是否使用GestureDetector.SimpleOnGestureListener类,必现实现onDown()  方法且该方法返回值为true, 由于所以的手势都是从onDown()消息開始的。假设onDown()函数返回false(GestureDetector.SimpleOnGestureListener的默认行为), 系统会觉得你系那个忽略接下来的手势,因此GestureDetector.onGestureListener中的其它函数就永远不会被调用。  onDown()返回fasle的唯一用处是你确实想忽略掉整个手势。一旦你实现了GestureDectecor.OnGestureListener接口而且创建了一个GestureDetector实例,你能够使用该实例来处理你在onTouchEvent()中接收到的触摸事件。
@Override public boolean onTouchEvent(MotionEvent event) {     boolean result = mDetector.onTouchEvent(event);     if (!result) {         if (event.getAction() == MotionEvent.ACTION_UP) {             stopScrolling();             result = true;         }     }     return result; }

当你给onTouchEvent()函数传递它不能识别为手势的触摸事件时,它会返回false。測试你能够执行自己定制的手势检測代码。
2)创建看似合理的移动
手势是控制触屏设备的强有力的工具, 可是它们有可能会违背直觉并难以被记住,假设它们不能产生看似合理的结果的话。如fling手势,该手势发生在用户在屏幕上高速的移动手指然后抬起它的时候。
UI的看似合理的响应应该是:在fling的方向上高速移动。然后慢慢减速,仿佛用户推动转速轮使其開始转动一样。可是。产生转速轮的感觉不是那么easy,须要大量的物理和数学知识来使得转速轮模式正确的执行。 为此,Android提供了辅助类Scroller  来处理fling手势。
開始fling操作前,调用fling()  函数,该函数须要x,y方向上的启动速度,最小和最大值等作为參数。对于速度值。能够使用GestureDector计算的数值:
@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(); }

注:虽然GestureDetector计算的速度在物理上是准确的,非常多开发人员任务使用这个值使得fling动画太快。因此通常使用速度值除以4到8以后的数值。

fling()调用设置fling手势的物理模型。
之后, 能够每隔一个固定时间段,通过调用  Scroller.computeScrollOffset()  来更新Scroller类。
  computeScrollOffset()读取当前时间。使用上述物理模型计算当前时间的x和y的值。然后更新Scroller对象的状态。
通过调用  getCurrX()  和getCurrY()  来获取这些值。

大多数view类对Scroller对象的x和y的位置信息的处理是直接传送给  scrollTo()函数。PieChart演示样例有一点不同:它使用y值来设置图表中的旋转角度:
if (!mScroller.isFinished()) {     mScroller.computeScrollOffset();     setPieRotation(mScroller.getCurrY()); }

Scroller  类计算位置信息,可是不会自己主动将这些信息应用到view类中。
开发人员须要确保及时获取并应用新的坐标位置到view中以保证滚动效果的流畅性。能够通过例如以下两种方法来做:

  • 在调用fling()后调用postInvalidate()  。强制系统重绘。这须要在onDraw() 中计算滚动偏移值并在每次滚动偏移值发生改变时调用postInvalidate()  。
  • 在fling过程中设置ValueAnimator  并通过调用  addUpdateListener()加入监听器来处理动画。

PieChart使用另外一种方法。该技术更复杂一些,可是利用了动画机制,避免了不必要的页面重绘。缺点是ValueAnimator类是在API 11開始使用的。因此不能在3.0下面的Android系统中执行。
        mScroller = new Scroller(getContext(), null, true);         mScrollAnimator = ValueAnimator.ofFloat(0,1);         mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {             @Override             public void onAnimationUpdate(ValueAnimator valueAnimator) {                 if (!mScroller.isFinished()) {                     mScroller.computeScrollOffset();                     setPieRotation(mScroller.getCurrY());                 } else {                     mScrollAnimator.cancel();                     onScrollFinished();                 }             }         });

3)平滑地状态过渡
用户希望UI状态的过渡更平滑一些,UI元素淡入和淡出而不是出现和消失。移动平滑地開始和结束而不是突然開始和停止。Android3.0版本号引入property animation framework, 以保证状态的平滑过渡。

当属性值的改变会改变view的外观时。使用  ValueAnimator  来改变属性值。亦即使用动画系统来改变属性值。
下例中,改动PieChart的当前选中饼图块会导致整个饼图旋转以便选中的饼图块居中。
ValueAnimator会每隔几百毫秒改动旋转角度,而不是马上设置新的旋转角度值。
mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0); mAutoCenterAnimator.setIntValues(targetAngle); mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION); mAutoCenterAnimator.start();

假设改动的属性是  View  的属性时,动画会更easy一些,由于View有一个内置的ViewPropertyAnimator  类,该类已被优化来处理多个属性值同一时候改变的动画。如:
animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();

四)对view进行优化为避免UI让人感觉迟滞,需保证动画持续地每秒播放60帧。

要想到达上述目的。须要在常常被调用的函数中去除不是必需的代码。首先检查onDraw()函数,须要去除该函数中的分配空间的操作,由于分配空间会导致垃圾收集操作,而后者会引起卡顿。能够在初始化或动画间隙分配对象,永远不要在动画过程中分配对象。
尽可能的降低onDraw()的使用频率。
大部分的onDraw()调用都是invalidate()引起的,因此须要避免不是必需的invalidate()调用。
另外一个花费时间的操作是遍历布局。每次view调用  requestLayout(), Android的UI系统都会遍历整个view树去找出每一个view的大小。假设发现冲突。可能会须要多次遍历view树。
UI设计者有时会构造非常深的嵌套ViewGroup树以使UI能够非常好的使用。
确保view树尽可能地浅。

假设UI非常复杂的话,能够考虑写一个定制的  ViewGroup  来运行布局操作。
不像内置的view类。自己写的定制view类能够对子控件给出详细的尺寸和形状,避免遍历子控件来计算尺寸。如在PieChart演示样例中,PieChart类包括子页面。可是从来不去计算他们的尺寸,而是依据定制布局算法直接设置它们的尺寸。









    推荐阅读