【Android应用开发技术:用户界面】自定义View类设计
2015-08-07 17:14
627 查看
作者:郭孝星
微博:郭孝星的新浪微博
邮箱:allenwells@163.com
博客:http://blog.csdn.net/allenwells
Github:https://github.com/AllenWells
遵循Android标准规则
提供自定义的风格属性值并能够被Android XML Layout所识别。
发出可访问的事件
能够兼容Android的不同平台
下面我们就来介绍如何一步步的去实现一个设计良好的类。
为自定义的View在资源标签下定义自设的属性
在XML Layout中指定属性值
在运行时获得属性值
把获取到的属性值应用到自定义的View上
定义自设属性,添加到res/values/attrs.xml文件中,如下所示:
以上定义了两个自设属性:showText和labelPosition,它们都归属于PieChat的项目下的styleable实例,styleable实例的名字通常和自定义View的名字一致。
当我们定义了自设的属性,我们就可以在Layout XML文件中使用它们,就像内置属性一样,唯一不同时自设属性归属于不容的命名空间,如下所示:
注意:
为了避免输入长串的namespace名字,示例上面使用了 xmlns 指令,这个指令可以指派custom作为 http://schemas.android.com/apk/res/com.example.customviews namespace的别名。我们也可以使用其他别名作为namespace。
如果你的view是一个Inner Class,我们需要指定这个View的Outer Class。同样的,如果PieChart有一个Inner Class叫做PieView。为了使用这个类中自设的属性,我们需要使用com.example.customviews.charting.PieChart$PieView。
拥有的属性资源并没有经过解析
styles并没有应用上
我们通过attrs的方法是可以直接获取到属性值的,但是不能确定值的类型,如下所示:
取而代之的方法是通过obtainStyledAttributes()方法来获取属性值,该方法会传递一个TypedArray对象,Android资源编译器对res目录里的每一个,自动生成R.java文件定义了存放属性ID的数组和常量,这些常量用来引用数组中的每个属性。我们可以通过TypedArray对象来读取这些属性。
注意:TypedArray对象是一个共享对象,使用完毕后应该进行回收。
除了暴露属性之外,我们还需要暴露事件,自定义的View也需要能够支持响应事件的监听器。
onDraw()方法会做以下常见操作:
绘制文字使用drawText()。指定字体通过调用setTypeface(), 通过setColor()来设置文字颜色.
绘制基本图形使用drawRect(), drawOval(), drawArc(). 通过setStyle()来指定形状是否需要filled, outlined.
绘制一些复杂的图形,使用Path类. 通过给Path对象添加直线与曲线, 然后使用drawPath()来绘制图形. 和基本图形一样,。是outlined, filled, both.
通过创建LinearGradient对象来定义渐变。调用setShader()来使用LinearGradient。
通过使用drawBitmap来绘制图片.
举例
Android Graphics Framework把绘制定义为下面两类:
Canvas:绘制什么
Paint:如何绘制
举例
创建Paint对象,定义颜色、样式和字体等。
View中有很多方法可以用来计算大小。
onSizeChanged()
onSizeChanged():当View第一次被赋予一个大小时,或者View的大小被更改时触发该方法,我们可以在该方法里计算位置、间距和其他View的大小值。
当我们的View被设置大小时,布局管理器会假定这个大小包括所有View的内边距(Padding),当我们计算View的大小时,我们需要处理内边距的值,如下所示:
onMeasure()
onMeasure()方法用来精确控制View的大小,该方法的参数是View.MeaureSpec,该参数会告知我们的View的父控件的大小。
注意:
计算的过程有把view的padding考虑进去。这个在后面会提到,这部分是view所控制的。
帮助方法resolveSizeAndState()是用来创建最终的宽高值的。这个方法会通过比较view的需求大小与spec值,返回一个合适的View.MeasureSpec值,并传递到onMeasure方法中。
onMeasure()没有返回值。它通过调用setMeasuredDimension()来获取结果。调用这个方法是强制执行的,如果我们遗漏了这个方法,会出现运行时异常。
常见的用户输入事件时Touch事件,多种Touch事件之间的相互作用称为Gesture,常见的Gesture有以下几种:
tapping
pulling
flinging
zooming
GestureDetector用来管理Gesture,它通过传入的GestureDetector.OnGestureListener来构建,如果我们只想处理简单的几种手势操作,我们也可以传入GestureDetector.SimpleOnGestureListener,如下所示:
不管我们是否使用GestureDetector.SimpleOnGestureListener, 我们总是必须实现onDown()方法,并返回true。因为所有的gestures都是从onDown()开始的。如果你在onDown()里面返回false,系统会认为我们想要忽略后续的gesture,那么GestureDetector.OnGestureListener的其他回调方法就不会被执行到了。
一旦我们实现了GestureDetector.OnGestureListener并且创建了GestureDetector的实例, 我们可以使用我们的GestureDetector来中止你在onTouchEvent里面收到的touch事件,如下所示:
下面我们来讨论如何提升一些常见方法的效率。
onDraw()方法
onDraw()方法,我们应该尽量减少onDraw()方法的调用,也即invalidate()方法的调用,如果真的有需求调用invalidate()方法,也应该调用带参数的invalidate()方法进行精确绘制,而不是无参数的invalidate()方法,因为无参数的invalidate()方法会绘制整个View。
requestLayout()方法
requestLayout()方法,会使得Android UI系统去遍历整个View的层级来计算出每一个view的大小。如果找到有冲突的值,它会需要重新计算好几次。另外需要尽量保持View的层级是扁平化的,这样对提高效率很有帮助。如果去设计一个复杂的UI,我们应该考虑写一个自定义的ViewGroup来执行它的layout操作。与内置的View不同,自定义的View可以使得程序仅仅测量这一部分,这避免了遍历整个View的层级结构来计算大小。
一旦你开启了硬件加速,性能的提示并不一定可以明显察觉到。移动设备的GPU在某些例如scaling,rotating与translating的操作中表现良好。但是对其他一些任务,比如画直线或曲线,则表现不佳。为了充分发挥GPU加速,我们应该最大化GPU擅长的操作的数量,最小化GPU不擅长操作的数量。
举例
绘制pie是相对来说比较费时的。解决方案是把pie放到一个子View中,并设置View使用LAYER_TYPE_HARDWARE来进行加速。
微博:郭孝星的新浪微博
邮箱: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>
注意:
为了避免输入长串的namespace名字,示例上面使用了 xmlns 指令,这个指令可以指派custom作为 http://schemas.android.com/apk/res/com.example.customviews namespace的别名。我们也可以使用其他别名作为namespace。
如果你的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 mediaplayer状态机
- Android 自定义ViewGroup 实现FlowLayout
- 2015年8月初iOS 8安装率碾压Android 5.0
- Android 软键盘盖住输入框的问题
- Android添加一个Native Service
- android:activity生命周期及几个主要函数应当做的事情
- Android - 视图Android应用(apk)签名
- Android PullToRefresh 使用详解
- Android实战经验之图像处理及特效处理的集锦(总结版)
- 让Android Studio 使用上vs的android模拟器
- Android+PHP+Mysql实现用户登录
- Android LayoutInflater深度解析 给你带来全新的认识
- Android NDK 开发
- smack for android 之登录openfire
- [转]Android加载图片堆栈溢出
- android studio右下角没有分支便签解决办法
- 即时通讯 - Android、iOS、J2EE服务端的非对称加密传输数据
- Android之Content Provider学习使用
- Android APK文件签名—keytool和jarsigner
- Android 自定义RecyclerView 实现真正的Gallery效果