Android原生已经提供了一系列控件,如TextView,EditText等,但是这些往往不能满足应用开发需求。当现有的控件满足开发需求的时候,就需要按照自己的需求自定义控件。自定义控件主要有以下几点:
1.继承自View或某个widget 2.定义属性及style 3.应用属性 4.定义属性事件方法 5.View的测量、布局及绘制 6.控件优化 7.其他细节问题
继承自View或某个widget
Android framework里面控件都是从View派生出来的。你自定义的view也可以直接继承View,或者你可以通过继承既有的一个子类(例如Button)来节约一点时间。 为了能够在layout xml文件中添加你自定义的控件,并使Android framework能够inflage自定义控件,你必须至少提供一个constructor,它包含一个Contenx与一个AttributeSet对象作为参数。
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
为什么一定要实现这个constructor? framwork 中inflate layout文件方法其实就是去解析xml文件,递归找到每一个TAG对应的类,反射获取对应类中带有参数Context和AttributeSet的构造函数,再通过这个构造函数去创建一个实例。所以如果你的控件中不包含这个constructor就会报错,在layout xml中使用自定义控件一定要把包名类名写完整。
定义属性及style
如果你的控件是继承自View,那View相关的属性可以一并继承过来,比如background,View已经有了这个属性,某些情况下涉及绘制相关的,你可以直接在xml指定控件背景,但是一些新增行为或样式,就需要通过自定义属性实现。 属性一般放置于res/values/attrs.xml文件中,下面是一个attrs.xml文件的示例:
<resources>
<declare-styleable name="CustomView">
<attr name="mCircleRadius" format="dimension" />
<attr name="mCircleColor" format="reference|color" />
</declare-styleable>
</resources>
上面的代码定义了两个属性,mCircleRadius与mCircleColor,它们都归属于CustomView这个styleable。styleable的名字,没有严格的要求,通常与自定义的控件名字一致。 mCircleRadius对应的格式是dimensions, mCircleColor则可以是一个具体的颜色值或引用。
定义了属性之后,就可以在layout xml文件中使用它们,具体用法跟使用原生属性一样,唯一不同的是自定义属性是归属于不同的命名空间。不是属于[http://schemas.android.com/apk/res/android的命名空间,重定义命名控件http://schemas.android.com/apk/res/http://schemas.android.com/apk/res/%5byour“>your package name] 或 http://schemas.android.com/apk/res-auto,实例如下
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:my="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.example.customviewdemo.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dip"
android:id="@+id/custom_view"
flyme:mCircleRadius="10dip"
flyme:mCircleColor="@android:color/darker_gray"
/>
</RelativeLayout>
需要注意:如果自定义的控件是一个内部类,比如CustomView的一个内部类InnerCustomView,则需要改成com.example.customviewdemo.CustomView$InnerCustomView
有时候同一个控件可能在多个地方用,如果每个地方都需要设置相同属性,那就会有很多重复的工作,一旦需要修改,需要一个个去修改。可以通过style解决这个问题。 在styles.xml文件中定义一个style
<style name="CustomView" >
<item name="mCircleColor">#eeee00</item>
<item name="mCircleRadius">50dip</item>
</style>
layout xml文件中就可以用style替换之前设置属性
<com.example.customviewdemo.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dip"
android:id="@+id/custom_view"
style="@style/CustomView"
/>
为了方便所有使用同一Theme主题的控件都能有统一的风格,可以将sytle添加到Theme主题中。 定义一个主题属性,注意styleable命名不要用Theme
<declare-styleable name="MyTheme">
<attr name="CustomViewStyle" format="reference"></attr>
</declare-styleable>
自定义主题,为CustomViewStyle属性指定一个style。
<resources>
<!--
Base application theme, dependent on API level. This theme is replaced
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
-->
<style name="AppTheme" parent="Theme.AppCompat.Light">
<item name="CustomViewStyle">@style/CustomView</item>
</style>
<style name="CustomView" >
<item name="mCircleColor">#eeee00</item>
<item name="mCircleRadius">50dip</item>
</style>
</resources>
在AndroidManifest.xml中应用自定义Theme
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
应用属性
当控件从layout xml被创建的时候,在xml标签下的属性值都是从resource下读取出来并传递到控件的constructor方法作为一个AttributeSet参数,然后通过Context方法obtainStyledAttributes()来获取属性值。这个方法会返回一个TypedArray对象,TypedArray可以看做一个属性集合,接着就可以从TypedArray中获取自定义属性的值。
public CustomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyle, 0);
mCircleColor = a.getColor(R.styleable.CustomView_mCircleColor, Color.BLACK);
mCircleRadiu = a.getDimensionPixelSize(R.styleable.CustomView_mCircleRadius,0);
a.recycle();
}
需要注意:TypedArray对象是一个共享资源,必须被在使用后进行回收。
添加属性事件方法
上面自定义的属性只能在xml中使用,但仅仅能够在view被初始化的时候被读取到。每一个属性基本都需要对应一个动态方法,所以还需要提供一系列的 getter与setter方法。
public int getCircleColor() {
return mCircleColor;
}
public int getCircleRadiu() {
return mCircleRadiu;
}
public void setCircleRadiu(int radiu){
if(radiu != mCircleRadiu){
mCircleRadiu = radiu;
requestLayout();
}
}
public void setCircleColor(int color){
if(color != mCircleColor){
mCircleColor = color;
invalidate();
}
需要注意:在setCircleColor方法里面有调用invalidate()触发控件重绘. 当控件的某些内容发生变化的时候,需要调用invalidate来通知系统对这个view进行重绘,在setCircleRadiu方法里面调用requestLayout();mCircleRadiu变化会引起组件大小变化时,需要调用requestLayout方法。调用invalidate()和requestLayout()之后系统并不是立刻就下执行,而是将需要重绘或者重新layout的请求传递到ViewRootImpl, 等待系统每隔16ms发出VSYNC信号(涉及Android Display相关),触发UI重新渲染,VSYNC信号到达之后回调ViewRootImpl类中TraversalRunnable这个callback去执行doTraversal方法,从而对当前窗口控件真正去执行layout或invalidate事件。必须确保每次渲染都能在16ms内完成,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps。所以我们需要尽可能让控件变得简单一些,如果每次渲染都会执行一些复杂的layout及绘制工作不仅仅加重CPU的负载,也会让界面变得卡顿,什么时候需要invalidate(),什么时候需要requestLayout()要区分清楚。
另外一点需要注意的是,不要在子线程中更新UI界面,比如调用setCircleRadius或setCircleColor,ViewRootImpl类中每次对View进行layout或重绘,都会检查是否主线程调用,如果你的应用在子线程中更新了View没有报错,可能两方面原因,你的View还有没有附加到窗口上,或者你的Activity已经处于stop状态,反正这两种情况下,你的请求都不会执行。子线程触发UI刷新可以用postIvalidate,postIvalidate本质上也是通过handler通知UI线程去执行。
除了基本的属性事件,有些情况下还需要支持响应事件的监听,例如点击、长按响应等。
View的测量、布局及绘制
了解measure、layout、draw的作用
measure
测量控件自身及子控件大小,measure过程中自上而下遍历所有子控件,最终每个控件测量得到的结果保存到mMeasuredWidth 和 mMeasuredHeight这两个变量中,可以通过getMeasuredWidth()和getMeasuredHeight()方法获得。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredWidth = mCircleRadiu * 2 + getPaddingLeft() +getPaddingRight();
int desiredHeight = mCircleRadiu * 2 + getPaddingTop() +getPaddingBottom();
int widthSize = resolveAdjustedSize(desiredWidth, getMinWidth(), widthMeasureSpec);
int heightSize = resolveAdjustedSize(desiredHeight, getMinHeight(), heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
}
layout
布局子控件的位置,例如LinearLayout中决定了子控件横向或纵向排布。子控件的具体位置都是相对于父容器而言的。 在 layout 过程中,控件会调用getMeasuredWidth()和getMeasuredHeight()方法获取到 measure 过程得到的 mMeasuredWidth 和 mMeasuredHeight,作为自己的 width 和 height。然后调用每一个子视图的layout(l, t, r, b)函数,来确定每个子视图在父视图中的位置。
draw
绘制控件外观,如背景,分割线,滚动条等等。
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
if(mCircleRadiu > 0){
canvas.restore();
Paint paint = new Paint();
paint.setColor(mCircleColor);
canvas.drawCircle(getWidth()/2, getHeight()/2, mCircleRadiu, paint);
canvas.save();
}
}
控件优化
1、不要在初始化方法中执行耗时操作,是否有必要加载资源,加载布局文件等等。 2、不要随意调用invalidate、requestLayout,局部刷新的情况下可调用 invalidate(Rect dirty)或invalidate(int l, int t, int r, int b)。 3、过度绘制,减少布局层次,减少没必要的背景。某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。 4、适当情况打开硬件加速,充分利用GPU的特性,使得绘制更加平滑,但是会多消耗内存,个别绘制操作不支持导致控件显示异常等问题,具体见http://developer.android.com/guide/topics/graphics/hardware-accel.html。 5、尽可能不要在onDraw方法去加载bitmap,内存分配可能会触发GC,导致卡顿。
其他需要注意的问题
2,控件命名,控件名最好是体现功能特点 2、控件中使用api要确认需要兼容的版本,@SuppressLint(“NewApi”)和@TargetApi()仅仅只是屏蔽lint报错,在方法中还要判断版本做兼容处理。 3、某些情况下可以通过控件组合,根据业务逻辑增加一些方法控制,不一定每一个都需要重新定义一个新的控件 4、某些情况下涉及绘制相关的,可以通过定制drawable实现,而不一定要定制控件,例如demo的CustomView,只是重新处理了绘制相关内容,完全可以定义一个drawable代替。这样对应用使用方便,扩展性更高。