Android框架为我们提供了大量的视图类来帮助我们做好展示信息以及同用户进行交互的工作。然后有时候,我们的app或许需要一些在Android内建视图之外特殊的视图,那么此时我们就需要自定义视图。下面我们来看看如何构建一个具有鲁棒性和可重用的视图。本文主要结合谷歌官方文档和API介绍自定义视图。

第一步:建立一个视图类

  1.1 继承自View作为View的一个子类

        一个设计良好的自定义视图类应该和其他设计良好的类一样:封装了丰富的功能、为用户提供了易用的接口、高效的使用CPU和内存等。此外,自定义视图应该:符合Android规范、利用XML提供自定义样式属性、发送访问事件已经兼容不同的Android平台。
        在Android框架中,所有的视图类都继承自View类,所有我们的自定义视图类也应该继承自View类或者某些View的子类(比如:Button)。
        为了便于ADT和我们的自定义视图进行交互,我们应该提供给一个至少包含Context和AttributeSet作为参数的构造函数。这样的一个构造函数也是布局编辑器可以创建和实例化我们的自定义视图。        
 class PieChart extends View {
public PieChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
}

  1.2  自定义属性

        当我们添加一个内建的视图时,我们使用XML中的元指定它的外观和行为。设计良好的自定义视图也应该可以通过XML进行添加以及设定样式。为此,我们应该:在一个<declare-styleable>资源中为我们的视图 定义自定义属性、在XML中为属性指定数值、运行时抽取属性值、将抽取的属性值应用于自定义视图。例如,添加一个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,它们都属于一个叫做“PieChart”的styleable实体。按照常理,styleable实体的名字应该和自定义视图的名字相同。
        当定义好自定义属性时,我们就可以像使用内建属性一样在布局XML文件中使用它们。唯一不同的是,自定义视图改变了命名空间,它不在属于
http://schemas.android.com/apk/res/android,而是属于http://schemas.android.com/apk/res/[your package name]。        
 <?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>
     注意:在XML标签中应该使用全名。尤其是当自定义视图是内部类时,必须用外部类来指明。例如:PieChart类有一个内部类叫做PieView,那么我们就应该使用<com.example.customviews.charting.PieChart$PieView>标签。

  1.3 应用自定义属性

        当根据XML布局穿件一个View时,所有XML标签中的属性都会被从资源bundle中读取并作为AttributeSet参数传递给构造函数。尽管可以直接从AttributeSet中读取各个属性的值,但是这么做有两个缺点:属性值中引用的资源不会被解析、样式不会被应用。
       推荐的做法是:将AttributeSet传递给obtainStyledAttributes()方法。这个方法会返回一个包含已经解析和定义好样式的TypeArray数组。
 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();
}
}
    TypeArray的作用就是取出每一个对应位置的属性值。我们需要在最后调用recycle()函数,一边以在下一次调用时重用。所有涉及TypeArray的操作都应该在recycle()被调用之前。  

  1.4 添加属性和事件

        Attribute可以方便我们定义视图的外观,但是它们只会在初始化时被读取。为了提供动态行为,我们可以为每个自定义属性提供一个getter和setter方法。下面的代码说明了如何提供一个叫做“showText”的属性。
 public boolean isShowText() {
return mShowText;
}
public void setShowText(boolean showText) {
mShowText = showText;
invalidate();
requestLayout();
}
   注意:需要在进行了可能会改变外观的操作之后调用invalidate()函数和requestLayout()函数,来使得系统重新绘制view,确定大小和形状。这也确保了视图行为的可靠性和可用性。
        

  1.5  无障碍

         Android提供了良好的机制来帮助有视觉、听觉以及其他身体限制的用户使用Android设设备。文字语音转换功能、触控反馈、轨迹球以及D-pad等都为用户提供了良好的辅助性功能。 有关详细内容请阅读:https://developer.android.com/guide/topics/ui/accessibility/index.html
 

第二步:自定义绘图

  2.1 重载onDraw()方法

   自定义视图最重要的部分便是它的外观。我们通过重载onDraw()方法来实现自定义的外观。传递给onDraw()方法的参数是Canvas对象,Canvas类定义了绘制文字、线、位图以及其他基本图形的方法。
            在调用所有的绘制方法之前,我们需要创建一个Paint对象。

  2.2 创建Drawing对象

  我们可以简单的做个类比:Canvas好比是一个画布(实际是一个bitmap, 我们自定义的视图都是在这个Bitmap上进行绘制的),而Paint是一个画笔(确定了颜色和样式等信息)。Canvas提供了绘制一条线的方法,而Piant提供了定义这条线颜色的方法。Canvas提供了绘制一个矩形的方法,而Paint定义了这个矩形是否需要填充。总结起来,Canvas定义了绘制在屏幕上的形状,而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));
...
  提前创建视图是一个很重要的优化的方法,View会被频繁的重新绘制,这会消耗昂贵的资源。如果在onDraw()方法中创建绘图相关的对象将会大大降低性能,导致UI卡顿。上面的代码在构造函数中被调用,从而避免了这个问题。

  2.3 处理布局事件

        为了绘制自定义视图,我们必须知道视图的大小。负责的自定义视图常常需要根据它们显示在屏幕上的大小和形状进行多个布局相关的计算。尽管View提供了处理尺寸的方法,但是其中大部分我们都不需要进行重载。如果我们不需要进行特殊的大小设置,我们只需要重载onSizeChanged()方法。onSizeChanged()会在View第一次被分配大小以及大小改变时被调用。计算位置,分隔符以及其他和View大小相关的操作都在onSizeChanged()中进行。在为View分配大小时,布局管理器默认包含了padding的大小。
        // 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()方法。这个方法的参数View.MeasureSpec会告诉父视图希望我们的视图的大小
以及是硬性最大值还是建议值。MeasureSpec代表了父类给子类的布局要求的封装。每一个MeasureSpec代表一个宽度和高度的要求,它是大小和模式(共三种模式:1.UNSPECIFIED:父类对子类的大小没有限制,子类可以获得期望的大小。 2.EXACTLY:父类为子类确定了一个确切的大小。3. AT_MOST: 只要不超出指定大小,子类想要多大就多大)的组合。
        例:PieChart试图是的自己足够大来来使得饼图和标签大小一样。    
 @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.正如前面提及的,这些计算中需要考虑padding。

2.resolveSizeAndState()方法用来确认最后个的宽度和高度。它会通过比较期望的大小和传递进onMeasure()方法的spec值来返回一个合适

的View.MeasureSpec。除非有引入一个不同大小的限制,否则它会去期望的数值。

3.onMeasure()方法没有返回值。它会在通过调用setMeasuredDimension()方法来传递结果。setMeasuredDimension()是强制需要的,如果没有调用它,会抛出运行时异常。

  2.4 绘制

  在创建了对象,并且定义的measure代码之后,我们可以开始实现onDraw()方法。每一个View的onDraw()方法都不相同,但是却大部分是有一些共同的操作的:
        1. 使用drawText()来绘制Text,使用setTypeface设置字体,使用setColor设置字体颜色。
        2. 使用drawRectangular()、drawOval()和drawArc()来绘制基本图形,通过setStyle()方法可以设置填空、边框等样式。
        3. 使用Path类来绘制复杂图形。通过调用drawPath()为Path添加直线和曲线来绘制图形。同样,可以通过setStyle()方法可以设置填空、边框等样式。
        4. 创建LinearGradient来定义渐变,调用setSharder()来使用LinearGradient构建渐变填充。
        5. 使用drawBitmap()来绘制bitmap。
 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);
}

  其中slice的角度计算如下

 for (Item it : mData) {
it.mStartAngle = currentAngle;
it.mEndAngle = (int) ((float) currentAngle + it.mValue * 360.0f / mTotal);
currentAngle = it.mEndAngle;
...
}

参考:http://developer.android.com/training/custom-views/index.html

Android自定义视图的更多相关文章

  1. Android自定义视图教程

    Android自定义视图教程 Android的UI元素都是基于View(屏幕中单个元素)和ViewGroup(元素的集合),Android有许多自带的组件和布局,比如Button.TextView.R ...

  2. Android自定义视图四:定制onMeasure强制显示为方形

    这个系列是老外写的,干货!翻译出来一起学习.如有不妥,不吝赐教! Android自定义视图一:扩展现有的视图,添加新的XML属性 Android自定义视图二:如何绘制内容 Android自定义视图三: ...

  3. Android自定义视图三:给自定义视图添加“流畅”的动画

    这个系列是老外写的,干货!翻译出来一起学习.如有不妥,不吝赐教! Android自定义视图一:扩展现有的视图,添加新的XML属性 Android自定义视图二:如何绘制内容 Android自定义视图三: ...

  4. Android自定义视图二:如何绘制内容

    这个系列是老外写的,干货!翻译出来一起学习.如有不妥,不吝赐教! Android自定义视图一:扩展现有的视图,添加新的XML属性 Android自定义视图二:如何绘制内容 Android自定义视图三: ...

  5. Android自定义视图一:扩展现有的视图,添加新的XML属性

    这个系列是老外写的,干货!翻译出来一起学习.如有不妥,不吝赐教! Android自定义视图一:扩展现有的视图,添加新的XML属性 Android自定义视图二:如何绘制内容 Android自定义视图三: ...

  6. 【转】ANDROID自定义视图——onLayout源码 流程 思路详解

    转载(http://blog.csdn.net/a396901990) 简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量——onMeasure():决定View的大小 2.布局 ...

  7. 【转】ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解

    原文地址:http://blog.csdn.net/a396901990/article/details/36475213 简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量—— ...

  8. ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解

    简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量--onMeasure():决定View的大小 2.布局--onLayout():决定View在ViewGroup中的位置 3. ...

  9. ANDROID自定义视图——onMeasure流程,MeasureSpec详解

    简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量——onMeasure():决定View的大小 2.布局——onLayout():决定View在ViewGroup中的位置 3. ...

随机推荐

  1. 删除小脚本 srm

    提示:只能删除当前路径下的目录或文件 #!/bin/bash #将测试好的脚本,拷贝到 $PATH 能够搜索到目录下.并且改名 例如: /usr/local/bin cp /test/srm.sh / ...

  2. [Recompose] Stream Props to React Children with RxJS

    You can decouple the parent stream Component from the mapped React Component by using props.children ...

  3. Jsoncpp使用具体解释以及链接问题解决

    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式. 易于人阅读和编写. 同一时候也易于机器解析和生成. 它基于JavaScript Programming ...

  4. Windows安装两个mysql数据库步骤

    因为新旧项目数据库版本号差距太大.编码格式不同.引擎也不同,所以仅仅好装两个数据库. 本次安装两个mysql数据库.版本号各自是4.0.18,5.5.36.都是可运行文件直接安装. 本机上之前已经安装 ...

  5. 两个对象值同样(x.equals(y) == true),但却可有不同的hash code,这句话对不正确?

    1.网上面试题 这是一道Java面试题.看了非常多答案都说不正确.能够看下面代码.就知道结果了 http://www.iteye.com/topic/485046第45题 答案是错误的 package ...

  6. 特定位取反(js实现)

    去华为面试的时候.没有做好准备工作.面试的流程没有问清也没有查,结果一过去就让上机做题,着实有点措手不及.笔者是擅长前端的Java Webproject师啊,主要的底层编程知识早已生疏了.机试题碰到了 ...

  7. html2canvas截取页面

    1.下载html2canvas.js 2.引入 3.修改html2canvas支持远程图片处理 function ImageContainer(src, cors) { this.src = src; ...

  8. OpenGL编程逐步深入(四)Shaders

    OpenGl 中的 Shader在一些中文书籍或资料中都被翻译为"着色器", 单从字面意思也看不出Shader到底是什么,Shader实际上就是一段代码,用于完成特定功能的一个模块 ...

  9. 超级硬件代理解决企业Web提速上网问题

    超级硬件代理解决企业Web提速上网问题 需求分析: XX集团是五家企业重组建立的特大型工程勘察设计咨询企业,下设10多个分公司,上网人数众多.有多台WEB server,对外服务,访问量及大.以前无论 ...

  10. android文本排布

    首先看一幅图,是简书App的一篇文章的截图,如下: 图1,图2 上面两个图片都是文本的显示,但是由于有多种格式,所以较为复杂,例如其中有普通文本,还有加粗的文本,还有图文混排的显示等等. 一.解析HT ...