Android自定义View(自定义控件)

    在 Android 的 framework 中已经有很多标准的 Views 类,用于实现显示数据及与用户交互的功能 。但是有时候,有些功能无法通过系统预定义的这些 views 来实现 。好在 Android 允许我们自定义 View,本章就来讲解一下如下创建一个健壮的、可利用的自定义 View 。
    我们将分几个小节来讲解自定义 View 的过程:
1. Creating a View Class
    即创建与 Android 内置 View 相似的类,并且使新创建的 View 类有自己的 attributes、支持 ADT layout editor 。
2. Custom Drawing
    利用 Android 的 graphics system,绘制独具特色的 View 。
3. Making the View Interactive
    即使创建的 View 能够流畅地响应用户的操作、手势等 。在这一小节讨论如何利用 gesture detection(手势监测)、physics 和 animation 来为用户提供操作流畅自然的体验 。
4. Optimizing the View
    如果自定义的 View 不能保持高帧率,那么即它再好看也没用 。这一小节讨论如何避免性能上的障碍,及如何利用硬件加速使自定义的 View 的 drawings 操作更快 。下面我们就逐一讨论这些步骤 。

一、Creating a View Class

    一个好的自定义 view 必须是和系统已实现的 view 是类似的,它要封装一系列的功能,并提供方便使用的用户接口,高效的使用 CPU 和内存资源等 。除了设计良好的 View 类之外,好的 custom view 还应该满足以下特征:
    * 符合 Android 的标准
    * 为 XML layouts 提供自定义的 styleable attributes
    * 可处理好事件
    * 能兼容各种 Android 平台

1. 继承 View 类
    在 Android framework 中,所有的 view 控件最终都继承了 View 类 。所以自定义 View 的时候,理所当然的要继承 View 类,当然也可以继承其子类,有时候这样可以省些麻烦 。
    为了使 Android Developer Tools(ADT)可以与自定义的 View 交互,在自定义的 View 类的构造方法中,至少要有一个 Context 和一个 AttributeSet 参数 。这样 layout editor 才能创建自定义 View 的实例并对其进行编辑 。示例代码如下
class PieChart extends View{
   
public PieChart(Context context,AttributeSet attrs){
       
super(context, attrs);
   
}
}

2. Define Custom Attributes
    在写好了自定义的 View 类后,就可以在布局文件中使用它,同时还可以使用 attributes 元素指定它的外观和行为 。设计良好的自定义 View 可通过 XML 文件设置它的样式,要通过 XML 文件来设置样式,你需要:
    * 通过在资源文件中,定义 <declare-styleable> 元素给自定义的 View 设置自定义的 attributes;
    * 给每个 attributes 设置好它的值;
    * 在运行时接收 attribute 的值;
    * 把接收到的 attribute 值应用到自定义的 View 上 。
    本节主要讲解如何定义自定义的 attributes 及如何给它们设置 values,下一节将讲解如何在运行时接收这些 attributes 并把它们应用到自定义的 View 上 。
    要定义自定义的 attributes,就要在资源文件中添加 <declare-styleable> 元素,通常是在  res/values/attrs.xml 文件中添加,如下是一个示例 attrs.xml 的代码
<resources>
   
<declare-styleable name="PieChart">
       
<attr name="showText" format="boolean"/>
       
<attr name="labelPosition" format="enum">
           
<enum name="left" value="0"/>
           
<enum name="right" value="1"/>
       
</attr>
   
</declare-styleable>
</resources>
    这个 styleable 实体名为 PieChart,它声明了两个自定义属性:showText 和 labelPosition 。通常按照习惯,我们会把这个实体名设置成和其对应 View 名相同的名字 。
    在定义好了自定义好的 attributes 后,就可以像使用 Android 内置的 attributes 一样在布局文件中使用它们了 。唯一不同的是自定义的 attributes 属于不同的namespace,所以你使用时还需要指定自己的命名空间,这个命名空间的格式为  http://schemas.android.com/apk/res/[your package name]
,如我们给 PieChart 类自定义了属性,那么在布局文件中我们可以这样用
<?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>
    为了方便,你也可以把名称设置成  http://schemas.android.com/apk/res-auto 。但是 res-auto 只能在 Gradle 下使用,如果你的开发工具不是 Gradle,那么命名空间就必须符合上边提到的格式,即 ...... + 完整的包名,如  http://schemas.android.com/apk/res/com.example.customviews
    注意:在 layout 中使用自定义的 View 的时候,必须指定它的完整类名,即包名+类名 。如果自定义的 View 是一个内部类,那么你还得指明它的外部类名 。例如,如果自定义的 View PieView 类是 PieChart 类的内部类,那么为了使用 PieView 类的自定义的 attributes,就必须这样指定其路径  com.example.customviews.charting.PieChart$PieView 。

3. 使用自定义属性
    当我们在布局文件中添加一个 View 的时候,系统就会从资源文件中把它所有的 attributes 读取出来,并封装成一个 AttributeSet 实例,然后传递到 view 类的构造方法中 。在构造方法中拿到这个实例后,我们可以直接从中读取出相关属性的值 。但是直接从 AttributeSet 对象中读取它的值是不好的,因为如果 AttributeSet 对象本身还引用了其它的 AttributeSet 对象,那么我们是无法读取到它所引用的 AttributeSet 对象的相关属性值的 。所以 Android 提供了  obtainStyledAttributes() 方法,我们把 AttributeSet 对象传递给这个方法,该方法就会帮我们读取其所有属性值,包括它可能引用的其它 AttributeSet 对象的属性值,最后这个方法会返回一个 TypedArray 对象 。
    对于资源文件里面的每个 <declare-styleable>,系统都会在 R.java 文件中为它的所有属性生成一个常量索引,所以你可以通过这些索引从 TypedArray 对象中读取到对应的值,如在 PieChart 类中,我们是这样读取它的 attributes 的:
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 对象是共享资源,使用完后必须回收它 。

4. Add Properties and Events
    虽然 Attributes 是控件 Views 的行为和外观的强大途径,但是它只能在 view 初始化的时候读取到 。所以为了能够实现动态的行为,我们得为自定义属性提供 getter 和 setter 方法,下面这段代码演示了如何为 PieChart 的 showText 提供 getter 和 setter
public boolean isShowText(){
   
return mShowText;
}

public void setShowText(boolean showText){
   mShowText
= showText;
   invalidate
();
   requestLayout
();
}
注意属性改变后,要调用 invalidate() 和 requestLayout() 通知系统重绘 view 。
    自定义的 View 也应该支持事件监听,这样才能和用户交互 。如在 PieChart 中,可通过调用 OnCurrentItemChanged 来通知 listeners 用户在转动 pie chart 了。

二、Custom Drawing

    本小节主要介绍如何绘制自定义的 View,如何处理其与用的响应等 。

1. Override onDraw()
    绘制自定义 View 的最主要步骤就是覆盖 onDraw() 方法了,onDraw() 方法接收的参数是一个 Canvas 对象,这个对象可以用来绘制 View 。Canvas 类定义了绘制文本、线条、bitmaps 有其它原始图像类型的方法,所以可以用 onDraw() 来创建自定义的 UI 。在使用 Canvas 的绘制方法进行绘制前,还需要创建 Paint 对象,稍后会介绍它的作用 。

2. Create Drawing Objects
    android.graphics 框架把绘制分成两部分:(1)Canvas:用于处理绘制什么;(2)Paint:用于处理如何绘制 。例如,Canvas 提供了一个绘制直线的方法,而 Paint 提供了定义线条颜色的方法;Canvas 提供了一个绘制矩形的方法,而 Paint 定义了是否对矩形进行颜色填充的方法等等。也就是说,Canvas 定义了它能在屏幕上绘制什么东西(图形),而 Paint 用于为图形定义绘制的属性,如颜色、样式等等 。
    所以在绘制之前,你要创建一个或多个 Paint 对象 。如在 PieChart 类中,我们定义了一个 init() 方法,并在这个方法中创建 Paint 对象,然后在构造方法中调用 init() 。
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(newBlurMaskFilter(8,BlurMaskFilter.Blur.NORMAL));

   
...
    提前创建 Paint 等对象对于优化是很有利的,因为 Views 可能需要频繁地重绘(redrawn),而很多 drawing 对象的创建开销是非常大的,所以在 onDraw() 里面创建 drawing 对象会降低性能,可能会导致界面出现卡顿的现象 。

3. Handle Layout Events
    在绘制自定义 View 的时候,你还得知道它的大小 。复杂的 View 通常需要计算很多 layout 的 size 及它们在屏幕上的位置等等  。千万不要把 View 的大小写死了,因为这样这个 View 就无法适配到不同 screen size、screen densities 的设备,也无法同时适配 portrait 和 landscape 。
    虽然 View 类有很多处理 measurement 的方法,但是没必要全部覆盖它们 。如果不需要精确控制 View 的 size,只需要覆盖 onSizeChanged() 方法就行了 。当 view 的 size 第一次被赋值的时候,它的  onSizeChanged() 方法就会被调用 。之后每次 view 的 size 发生变化的时候, onSizeChanged() 都会被重新调用 。所以要在这个方法中计算 positions、dimensions 及其它与 view 的 size 有关的属性值,而不是在每次绘制的时候去计算 。在 PieChart 类中,我们在  onSizeChanged() 方法中计算矩形的边界、文本的相对位置及其它可见元素的位置、大小等等 。
    当 view 的 size 被赋值后,layout manager 总是认为这个 size 已包括了该 view 的所有 padding 了,所以在计算 view 的 size 的时候,必须要把 padding 值也处理好 。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);
    如果要精确控制 view 的所有 layout 参数,那么就要实现  onMeasure() 方法了 。在这个方法中,可通过  View.MeasureSpec 参数知道自定义的 view 在父 view 中的大小是多少,这个大小是写死了的还是建议的大小等 。下面是 PieChart 的 onMeasure() 方法的实现,它总是尽可能的使得矩形区域变得更大
@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)计算的时候,要把 view 的 padding 也算进去,在绘制 view 的时候,不要忘了这点;
(2)辅助方法是  resolveSizeAndState() 用于确定最终的 width 和 height ;
(3)onMeasure() 方法没有返回值,在方法的末尾,必须要调用  setMeasuredDimension() 方法,把计算结果传递出去,如果没调用这个方法,会导致运行时异常 。

4. Draw!
    在创建好相关的 object,定义好 measuring 后,就可以实现 onDraw() 方法了 。虽然各 view 在这个方法中所做的事情是不同的,但是它们通常都会有以下操作:
(1)通过  drawText() 绘制文本,并调用  setTypeface() 设置文本属性,调用  setColor() 设置文本颜色;
(2)调用  drawRect() drawOval() , and  drawArc() 等绘制原始图形,并通过调用  setStyle() 设置图形是否填充等属性;
(3)如果要绘制更复杂的图形,就用  Path 类 。定义好 Path 的各顶点后,调用  drawPath() 来进行绘制,并通过  setStyle() 设置是否填充等 。
(4)调用  drawBitmap() 绘制 bitmaps 。
    
    下面是 PieChart 类的 onDraw() 方法
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);
}

三、Making the View Interactive

    通过前面的几节,我们已经能够完整地把自定义的 View 给画好了,下面就是处理它怎么与用户交互的问题了 。

1. Handle Input Gestures
    和其它的 UI 框架一样,Android 有自己的输入事件处理模型 。用户的操作会被转换成 events,然后触发相应的回调方法 。可以根据需要覆盖相关的回调方法,最常见的输入事件是 touch,它会触发  onTouchEvent(android.view.MotionEvent) 方法,如下
   @Override
   
publicboolean onTouchEvent(MotionEventevent){
   
returnsuper.onTouchEvent(event);
   
}
    TouchEvent 本身并没有太多用处,但是可以由它合成转换成很多有用的事件(手势),如  tapping, pulling, pushing, flinging, and zooming,为了把原始的 touch event 转换成手势,Android 提供了一个专门的类  GestureDetector 。
     GestureDetector 类的构造方法需要一个实现了  GestureDetector.OnGestureListener 的实例作参数 。如果只需要处理几个简单的手势,那么只要继承  GestureDetector.SimpleOnGestureListener 就行了,不需要实现  GestureDetector.OnGestureListener 接口 。如下面的这个类继承了  GestureDetector.SimpleOnGestureListener 类并覆盖了  onDown(MotionEvent) 方法 。
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() 消息开始的 。如果这个方法返回 false,那么系统就会忽略之后所有的手势,这样  GestureDetector.OnGestureListener 的其它方法就永远不会被调用到 。所以除非你真的要忽略掉所有的手势,否则千万不要在 onDown() 中返回 false 。在实现了  GestureDetector.OnGestureListener 并创建了  GestureDetector 的实例后,就可以用这个实例解析在  onTouchEvent() 中接收到的 touch events 了 。

    在上面的代码片段中, 调用 onTouchEvent(event) 的时候,如果这个方法返回 false,那么就说明它不认识这个手势(不认识这个 event),这时就可以调用自定义的处理代码了 。

2. Create Physically Plausible Motion
    在触屏设备上,手势的功能是非常强大的,但是在定义手势的时候,它必须符合人们日常的操作习惯 。如飞触手势,要让用户感觉到在快速滑动的时候,屏上的控件会飞快的滑动,当用户的手指离开屏幕后,控件能逐渐减速,直到停止滑动 。要做到这点并不容易,因为这涉及很多数字和物理计算,但是幸运的是 Android 系统提供了辅助的类,来实现这种模拟行为 。可用于帮助模拟飞触手势的类是  Scroller 类 。
    要开始飞触,就调用  fling() 方法,把起始速率(velocity)、最大最小的 x、y 值等传递给它,对于 velocity 的值,可以用 GestureDetector 类的计算结果 。
@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 计算的 velocity 值是非常精确的,但是很多开发都认为,根据这个值设置的飞触动画速度太快,所以很多开发都习惯把它除以 4 到 8 之间的任意一个整数 。
    调用 fling() 方法开始了飞触手势,之后,还要周期性地调用  Scroller.computeScrollOffset() ,这个方法会根据当前的时间和它的物理模型,计算 x、y 的新值(可以通过  getCurrX()  and  getCurrY() 来获取到这两个值),然后更新  Scroller 对象的内部状态 。
    大多数 views 会把新的 x、y 值直接传递给  scrollTo() 方法,但是 PieChart 有点不同,它用当前的 scroll y 值来设置圆形的旋转角度 。
if(!mScroller.isFinished()){
    mScroller
.computeScrollOffset();
    setPieRotation
(mScroller.getCurrY());
}
     Scroller 类会计算旋转位置,但是它并不会把计算结果直接应用到你的 view 上,所以在拿到新的值之后,要主动通知 view 进行重绘 。在两种方法进行重绘
(1)在调用 fling() 方法后,调用  postInvalidate() 方法强制重绘,要采用这种方法,就得在 onDraw() 中计算好 scroll offsets,并在每次 scroll offset 改变后调用  postInvalidate() 。
(2)用  ValueAnimator 类来启动过程动画,并通过调用  addUpdateListener() 给它提供 listener,用于监听 animation 的状态  。
    PieChart 采用了第二种方法,这个方法使用起来有点麻烦,但是它更像 animation 系统,模仿得更逼真 。而且它能自动避免了不必要的 invalidation 操作 。但是在 Android API level 11(Android 3.0) 以前,是没有  ValueAnimator 类的 。所以如果使用了这种方法,就要在运行时检查一下系统版本,如果 API level 低于 11,就不要调用这个类的功能 。
       mScroller = new Scroller(getContext(),null,true);
       mScrollAnimator
= ValueAnimator.ofFloat(0,1);
       mScrollAnimator
.addUpdateListener(newValueAnimator.AnimatorUpdateListener(){
           
@Override
           
publicvoid onAnimationUpdate(ValueAnimator valueAnimator){
               
if(!mScroller.isFinished()){
                   mScroller
.computeScrollOffset();
                   setPieRotation
(mScroller.getCurrY());
               
}else{
                   mScrollAnimator
.cancel();
                   onScrollFinished
();
               
}
           
}
       
});

3. Make Your Transitions Smooth
    从 Android 3.0 开始,引进了  property animation framework ,这个框架使得 UI 的状态转换、界面切换等变得更加流畅平滑 。当某些属性(property)的改变,会导致你的 view 的外观要相应发生改变的时候,不要直接把新的 property 直接应用到你的 view 上,可以使用  ValueAnimator 来更新 view 的 property 。示例代码如下
mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this,"PieRotation",0);
mAutoCenterAnimator
.setIntValues(targetAngle);
mAutoCenterAnimator
.setDuration(AUTOCENTER_ANIM_DURATION);
mAutoCenterAnimator
.start();
//////
animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();

四、Optimizing the View

    为了确保 UI 流畅,不出现卡顿现象,至少要保证 animations 的帧率在 60fps 以上,所以只是把自定义 View 画出来了还是不够的,我们还要在优化上下功夫 。
    为了让 view 更流畅,通常不要在频繁执行的代码里面做没必要的操作 。onDraw() 就是频繁执行的方法,所以对它的优化是非常重要的,尽量不要在这个方法里面执行 allocations 操作,因为 allocactions 操作可能会触发系统进行垃圾回收,而垃圾回收可能会导致卡顿现象 。所以尽量在初始化的时候就完成相关 objects 的初始化,animations 运行起来后就不用再初始化了 。
    除了要保证 onDraw() 方法不执行没必要的操作外,还要保证对它的调用不要太频繁,通常调用  invalidate() 方法会导致 onDraw() 被调用,所以如果没必要的话,就不要调用  invalidate() 方法 。
    此外,遍历 layouts 也是一个非常耗时的操作,每次 view 调用  requestLayout() 方法的时候,Android 的 UI 系统就要遍历整个 view hierarchy,计算每个 view 的尺寸、位置(即 measurments)等 。如果 measurments 的时候发现冲突,那么就需要多次遍历 view hierarchy 。在设计 layout 的时候,尽量不要嵌套太多层,因为嵌套的层次越深,遍历的时间越长,性能越低 。
    如果要实现的 UI 很复杂,最好考虑自定义  ViewGroup ,这样就可以根据自己的算法来确定  ViewGroup 中每个 child 的大小和位置,系统就不用对复杂的 UI 进行繁琐的 measurements 过程 。PieChart 中演示了如何使用自定义的  ViewGroup 来组织所有的 views,PieChart 也有 child views,但是却不需要对它们进行 measures 过程,而是直接根据自己的算法来计算它们的大小及位置 。

    下载 PieChart Demo 源码


你可能感兴趣的:(android,view,自定义控件,自定义view)