【Android应用开发技术:用户界面】自定义View类设计

原创 2015年08月07日 17:14:58

作者:郭孝星
微博:郭孝星的新浪微博
邮箱:allenwells@163.com
博客:http://blog.csdn.net/allenwells
Github:https://github.com/AllenWells

【Android应用开发技术:用户界面】章节列表

设计良好的类总是相似的,它使用一个易用的接口来封装一个特定的功能,它能有效的使用CPU和内存,我们在设计View类时,通常会考虑以下因素:

  • 遵循Android标准规则
  • 提供自定义的风格属性值并能够被Android XML Layout所识别。
  • 发出可访问的事件
  • 能够兼容Android的不同平台

下面我们就来介绍如何一步步的去实现一个设计良好的类。

一 继承一个View类

Android Framework里的View类都继承于View,我们自定义的View可以直接继承View或者其他View的子类。为了能够让ADT识别我们的View,我们必须至少提供一个构造器,如下所示:

class PieChart extends View {
    public PieChart(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

二 定义自设属性

为了添加一个内置的View到UI上,我们需要通过XML属性来指定它的样式和行为,良好的自定义View可以通过XML添加和改变样式,为了达到这种效果,我们通常会考虑:

  • 为自定义的View在资源标签下定义自设的属性
  • 在XML Layout中指定属性值
  • 在运行时获得属性值
  • 把获取到的属性值应用到自定义的View上

定义自设属性,添加到res/values/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>

以上定义了两个自设属性:showText和labelPosition,它们都归属于PieChat的项目下的styleable实例,styleable实例的名字通常和自定义View的名字一致。

当我们定义了自设的属性,我们就可以在Layout XML文件中使用它们,就像内置属性一样,唯一不同时自设属性归属于不容的命名空间,如下所示:

<?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>

注意

  1. 为了避免输入长串的namespace名字,示例上面使用了 xmlns 指令,这个指令可以指派custom作为 http://schemas.android.com/apk/res/com.example.customviews namespace的别名。我们也可以使用其他别名作为namespace。
  2. 如果你的view是一个Inner Class,我们需要指定这个View的Outer Class。同样的,如果PieChart有一个Inner Class叫做PieView。为了使用这个类中自设的属性,我们需要使用com.example.customviews.charting.PieChart$PieView。

三 应用自设属性

当View从XML Layout被创建的时候,在XML标签下的属性值都是从res下读取出来并传递到View的构造器作为一个AttributeSet的参数,尽管可以从AttributeSet中直接读取数值,但这样做有以下弊端:

  • 拥有的属性资源并没有经过解析
  • styles并没有应用上

我们通过attrs的方法是可以直接获取到属性值的,但是不能确定值的类型,如下所示:

//通过此方法可以获取title的值,但是不知道它的类型,处理起来很容易出问题。
String title = attrs.getAttributeValue(null, "title");
int resId = attrs.getAttributeResourceValue(null, "title", 0);
title = context.getText(resId));

取而代之的方法是通过obtainStyledAttributes()方法来获取属性值,该方法会传递一个TypedArray对象,Android资源编译器对res目录里的每一个,自动生成R.java文件定义了存放属性ID的数组和常量,这些常量用来引用数组中的每个属性。我们可以通过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对象是一个共享对象,使用完毕后应该进行回收。

四 添加属性和事件

Attributes是一个强大的控制View行为和外观的方法,但是它仅仅能够在View被初始化的时候被读取到,为了提供一个动态的行为,我们需要设置一些set和get方法,如下所示:

public boolean isShowText() {
   return mShowText;
}
public void setShowText(boolean showText) {
   mShowText = showText;

   //invalidate()和requestLayout()两个方法的调用是确保稳定运行的关键。当
   //View的某些内容发生变化的时候,需要调用invalidate来通知系统对这个View
   //进行redraw,当某些元素变化会引起组件大小变化时,需要调用requestLayout
   //方法。调用时若忘了这两个方法,将会导致hard-to-find bugs。
   invalidate();
   requestLayout();
}

除了暴露属性之外,我们还需要暴露事件,自定义的View也需要能够支持响应事件的监听器。

五 绘制View的外观

5.1 重写onDraw()方法

5.1.1 创建绘制对象

绘制一个自定义View的外观最重要的步骤是重写onDraw(),onDraw()的参数是一个Canvas对象,Canvas对象定义了绘制文本、线条、图像和许多其他图形的方法。

onDraw()方法会做以下常见操作:

  • 绘制文字使用drawText()。指定字体通过调用setTypeface(), 通过setColor()来设置文字颜色.
  • 绘制基本图形使用drawRect(), drawOval(), drawArc(). 通过setStyle()来指定形状是否需要filled, outlined.
  • 绘制一些复杂的图形,使用Path类. 通过给Path对象添加直线与曲线, 然后使用drawPath()来绘制图形. 和基本图形一样,。是outlined, filled, both.
  • 通过创建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);
}

Android Graphics Framework把绘制定义为下面两类:

  • Canvas:绘制什么
  • Paint:如何绘制

举例

创建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));

5.1.2 处理布局事件

为了正确的绘制自定义的View,我们需要知道View的大小。复杂的自定义View通常需要根据在屏幕上的大小与形状执行多次layout计算。而不是假设这个view在屏幕上的显示大小。即使只有一个程序会使用自定义View,仍然是需要处理屏幕大小不同,密度不同,方向不同所带来的影响。

View中有很多方法可以用来计算大小。

  • onSizeChanged()

onSizeChanged():当View第一次被赋予一个大小时,或者View的大小被更改时触发该方法,我们可以在该方法里计算位置、间距和其他View的大小值。

当我们的View被设置大小时,布局管理器会假定这个大小包括所有View的内边距(Padding),当我们计算View的大小时,我们需要处理内边距的值,如下所示:

// 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()

onMeasure()方法用来精确控制View的大小,该方法的参数是View.MeaureSpec,该参数会告知我们的View的父控件的大小。

@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);
}

注意

  • 计算的过程有把view的padding考虑进去。这个在后面会提到,这部分是view所控制的。
  • 帮助方法resolveSizeAndState()是用来创建最终的宽高值的。这个方法会通过比较view的需求大小与spec值,返回一个合适的View.MeasureSpec值,并传递到onMeasure方法中。
  • onMeasure()没有返回值。它通过调用setMeasuredDimension()来获取结果。调用这个方法是强制执行的,如果我们遗漏了这个方法,会出现运行时异常。

六 处理输入手势

Android提供一个输入事件的模型,用户的动作会转换成触发一些回调函数的事件,我们可以通过重写这些回调方法来处理用户的饿输入事件。

常见的用户输入事件时Touch事件,多种Touch事件之间的相互作用称为Gesture,常见的Gesture有以下几种:

  • tapping
  • pulling
  • flinging
  • zooming

GestureDetector用来管理Gesture,它通过传入的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());

不管我们是否使用GestureDetector.SimpleOnGestureListener, 我们总是必须实现onDown()方法,并返回true。因为所有的gestures都是从onDown()开始的。如果你在onDown()里面返回false,系统会认为我们想要忽略后续的gesture,那么GestureDetector.OnGestureListener的其他回调方法就不会被执行到了。

一旦我们实现了GestureDetector.OnGestureListener并且创建了GestureDetector的实例, 我们可以使用我们的GestureDetector来中止你在onTouchEvent里面收到的touch事件,如下所示:

@Override
public boolean onTouchEvent(MotionEvent event) {
   boolean result = mDetector.onTouchEvent(event);
   if (!result) {
       if (event.getAction() == MotionEvent.ACTION_UP) {
           stopScrolling();
           result = true;
       }
   }
   return result;
}

七 优化View性能

7.1 提升方法效率

为了设计良好的View,我们的View应该能执行的更快,不出现卡顿,动画也应该保持在60fps。为了加速我们的View,对于频繁调用的方法,应该尽量减少不必要的方法,在初始化或者动画间隙做内存非配的工作。

下面我们来讨论如何提升一些常见方法的效率。

  • onDraw()方法

onDraw()方法,我们应该尽量减少onDraw()方法的调用,也即invalidate()方法的调用,如果真的有需求调用invalidate()方法,也应该调用带参数的invalidate()方法进行精确绘制,而不是无参数的invalidate()方法,因为无参数的invalidate()方法会绘制整个View。

  • requestLayout()方法

requestLayout()方法,会使得Android UI系统去遍历整个View的层级来计算出每一个view的大小。如果找到有冲突的值,它会需要重新计算好几次。另外需要尽量保持View的层级是扁平化的,这样对提高效率很有帮助。如果去设计一个复杂的UI,我们应该考虑写一个自定义的ViewGroup来执行它的layout操作。与内置的View不同,自定义的View可以使得程序仅仅测量这一部分,这避免了遍历整个View的层级结构来计算大小。

7.2 使用硬件加速

从Android 3.0开始,Android的2D图像系统可以通过GPU (Graphics Processing Unit)来加速。GPU硬件加速可以提高许多程序的性能。但是这并不是说它适合所有的程序。Android Framework让我们能够随意控制你的程序的各个部分是否启用硬件加速。

一旦你开启了硬件加速,性能的提示并不一定可以明显察觉到。移动设备的GPU在某些例如scaling,rotating与translating的操作中表现良好。但是对其他一些任务,比如画直线或曲线,则表现不佳。为了充分发挥GPU加速,我们应该最大化GPU擅长的操作的数量,最小化GPU不擅长操作的数量。

举例

绘制pie是相对来说比较费时的。解决方案是把pie放到一个子View中,并设置View使用LAYER_TYPE_HARDWARE来进行加速。

private class PieView extends View {
       public PieView(Context context) {
           super(context);
           if (!isInEditMode()) {
               setLayerType(View.LAYER_TYPE_HARDWARE, null);
           }
       }
       @Override
       protected void onDraw(Canvas canvas) {
           super.onDraw(canvas);
           for (Item it : mData) {               mPiePaint.setShader(it.mShader);
               canvas.drawArc(mBounds,
                       360 - it.mEndAngle,
                       it.mEndAngle - it.mStartAngle,
                       true, mPiePaint);
           }
       }
       @Override
       protected void onSizeChanged(int w, int h, int oldw, int oldh) {
           mBounds = new RectF(0, 0, w, h);
       }
       RectF mBounds;
   }

【Android应用开发技术:用户界面】用户界面基本原理

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells Github:https://githu...
  • AllenWells
  • AllenWells
  • 2015年07月31日 17:07
  • 1161

【Android应用开发技术:用户界面】ViewAnimator

作者:郭嘉 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells github:https://github.com/AllenWel...
  • AllenWells
  • AllenWells
  • 2015年05月07日 21:05
  • 486

【Android应用开发技术:用户界面】SurfaceHolder

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells Github:https://githu...
  • AllenWells
  • AllenWells
  • 2015年07月31日 16:52
  • 723

【Android应用开发技术:用户界面】ImageView

作者:郭嘉 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells Github:https://github.com/AllenWel...
  • AllenWells
  • AllenWells
  • 2015年05月07日 21:00
  • 415

【Android应用开发技术:用户界面】TextView

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells Github:https://githu...
  • AllenWells
  • AllenWells
  • 2015年05月07日 21:02
  • 670

【Android应用开发技术:用户界面】布局管理器

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells Github:https://githu...
  • AllenWells
  • AllenWells
  • 2015年08月17日 17:19
  • 1190

【Android应用开发技术:用户界面】AdapterView

作者:郭嘉 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells github:https://github.com/AllenWel...
  • AllenWells
  • AllenWells
  • 2015年05月07日 21:04
  • 479

【Android应用开发技术:用户界面】Menu

作者:郭嘉 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells github:https://github.com/AllenWel...
  • AllenWells
  • AllenWells
  • 2015年05月07日 21:03
  • 465

【Android应用开发技术:用户界面】Dialog

作者:郭嘉 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells github:https://github.com/AllenWel...
  • AllenWells
  • AllenWells
  • 2015年05月07日 21:05
  • 410

【Android应用开发技术:用户界面】ActionBar

作者:郭嘉 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells github:https://github.com/AllenWel...
  • AllenWells
  • AllenWells
  • 2015年05月07日 21:06
  • 483
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:【Android应用开发技术:用户界面】自定义View类设计
举报原因:
原因补充:

(最多只允许输入30个字)