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

来源:互联网 发布:java手机游戏51 编辑:程序博客网 时间:2024/05/25 05:36

作者:郭孝星
微博:郭孝星的新浪微博
邮箱: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 paddingfloat xpad = (float)(getPaddingLeft() + getPaddingRight());float ypad = (float)(getPaddingTop() + getPaddingBottom());// Account for the labelif (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的父控件的大小。

@Overrideprotected 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事件,如下所示:

@Overridepublic 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;   }
0 0
原创粉丝点击