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

作者:郭孝星

微博:郭孝星的新浪微博

邮箱:[email protected]

博客:http://blog.csdn.net/allenwells

Github:https://github.com/AllenWells

设计良好的类总是相似的,它使用一个易用的接口来封装一个特定的功能,它能有效的使用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;
   }

版权声明:当我们认真的去做一件事的时候,就能发现其中的无穷乐趣,丰富多彩的技术宛如路上的风景,边走边欣赏。

时间: 2024-08-25 04:05:17

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

Android软件开发之盘点自定义View界面大合集(二)

Android软件开发之盘点自定义View界面大合集(二) - 雨松MOMO的程序世界 - 51CTO技术博客 雨松MOMO带大家盘点Android 中的自定义View界面的绘制 今天我用自己写的一个Demo 和大家详细介绍一个Android中自定义View中的使用与绘制技巧. 1.自定义view绘制字符串 相信在实际开发过程中必然很多地方都须要用到系统字 为什么会用到系统字? 方便 省内存 我相信做过J2ME游戏开发的朋友应该深知内存有多么多么重要 而且使用它还可以带来一个更重要的好处就是很方

Android Studio开发基础之自定义View组件

一般情况下,不直接使用View和ViewGroup类,而是使用使用其子类.例如要显示一张图片可以用View类的子类ImageView,开发自定义View组件可分为两个主要步骤: 一.创建一个继承自android.view.View类的View类,并且重写构造方法. 如下,新建一个名为MyView.Java的Java类文件,重写一个带Context的构造方法和onDraw()方法(用来重新绘制Activity窗口的背景). package com.example.lhb.contentprovid

Android中实现Bitmap在自定义View中的放大与拖动

一:基本实现思路 基于View类实现自定义View –MyImageView类.在使用View的Activity类中完成OnTouchListener接口,实现对自定义View的触摸事件监听 放大与拖动 基于单点触控实现Bitmap对象在View上的拖动.并且检测View的边缘,防止拖动过界.基于两个点触控实现Bitmap对象在View上的放大.并且检测放大倍数.基于Matrix对象实现对Bitmap在View上放大与平移变换 Bitmap对象在View中的更新与显示 通过重载onDraw方法,

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

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:[email protected] 博客:http://blog.csdn.net/allenwells Github:https://github.com/AllenWells [Android应用开发技术:用户界面]章节列表 [Android应用开发技术:用户界面]用户界面基本原理 [Android应用开发技术:用户界面]设备适配 [Android应用开发技术:用户界面]用户界面布局技巧 [Android应用开发技术:用户界面]View基本原理 [

【Android应用开发技术:用户界面】界面设计中易混淆的概念汇总

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:[email protected] 博客:http://blog.csdn.net/allenwells Github:https://github.com/AllenWells [Android应用开发技术:用户界面]章节列表 一 px.dp.sp px:即像素,每个px对应屏幕上的一个点. dp:即设备独立像素,一种基于屏幕密度的抽象单位,在每英寸160点的显示器上:1 dp = 1 px. sp:即比例像素,主要用来处理字体大小,可以根据用户字体

【Android应用开发技术:用户界面】9Patch图片设计

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:[email protected] 博客:http://blog.csdn.net/allenwells Github:https://github.com/AllenWells [Android应用开发技术:用户界面]章节列表 9Patch图片是一种特殊的PNG图片,该图片以.9.png为后缀名,它在原始图片四周各添加一个宽度为1像素的线条,这4条线决定了该图片的缩放规则和内容显示格则. 一 9Patch图片的显示规则 9Patch图片left边和t

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

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:[email protected] 博客:http://blog.csdn.net/allenwells Github:https://github.com/AllenWells [Android应用开发技术:用户界面]章节列表 布局管理继承于ViewGroup.它用来管理Android应用用户界面里各组件,它的使用使得Android应用的图形用户界面具有良好的平台无关性. 常见的布局方式例如以下所看到的: 线性布局 表格布局 帧布局 相对布局 网络布

【Android应用开发技术:图像处理】Bitmap显示性能优化分析

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:[email protected] 博客:http://blog.csdn.net/allenwells Github:https://github.com/AllenWells [Android应用开发技术:图像处理]章节列表 Bitmap经常会消耗大量内存而导致程序崩溃,常见的异常如下所示:java.lang.OutofMemoryError:bitmap size extends VM budget,因此为了保证程序的稳定性,我们应该小心处理程序

[Android游戏开发学习笔记]View和SurfaceView

本文为阅读http://blog.csdn.net/xiaominghimi/article/details/6089594的笔记. 在Android游戏中充当主要角色的,除了控制类就是显示类.而在Android中涉及到显示的是View类,及继承自它的SurfaceView类和SurfaceView的其他子类等. 这里先只说View和SurfaceView.SurfaceView的直接子类有GLSurfaceView和VideoView,可以看出GL和视频播放以及CAmera摄像头一般均使用Su