如果想要做出绚丽的界面效果仅仅靠系统的控件是远远不够的,这个时候就必须通过自定义View来实现这些绚丽的效果。自定义View是一个很难掌握的技术体系,包括View的层次结构、事件分发机制和View的工作原理等细节。自定义View的实现方法有很多,当面对一个自定义View的需求时,需要灵活地分析从而找到最高效的方法。
自定义View中有些问题如果处理不好会影响View的正常使用或者导致内存泄漏。大概需要注意一下几点:
·让View支持wrap_parent
直接继承自View或者ViewGroup的控件,如果不在onMearsure中对wrap_content做特殊处理,在布局中使用wrap_parent时将无法产生想要的效果。这在View的工作原理(一)中有详细解释,直接继承View的自定义控件需要重写onMearsure方法并设置wrap_content时自身的大小,否则使用wrap_content时相当于使用march_parent。为什么这么说,需要结合SpecMode解释,当View使用wrap_content时他的SpecMode也就是最大模式,这种情况下View的specSize是父容器中目前剩余的大小,很显然View的宽高等于父容器当前剩余的大小,效果和match_parent一致。那么解决方法就是在重写的onMearsure方法中给wrap_content模式的View设置默认宽高就行了。在View的工作原理(一)中有代码示例。
·让View支持padding
直接继承自View的控件如果不在draw方法中处理padding,那么padding属性是无法起作用的。另外直接继承自ViewGroup的控件需要在onMearsure和onLayout中考虑padding和子元素margin对其造成的影响,否者padding和子元素margin失效。
·尽量不要在View中使用Handler
View内部本身就提供了post系列的方法,完全可以代替Handler。
·View中如果有线程或者动画,需要及时停止
如果View变得不可见时我们要及时的停止线程和动画,否者可能造成内存泄漏。
·View带有滑动嵌套的时候需要处理好滑动冲突
比如一个自定义ViewGroup,其中有很多子元素,子元素可以左右滑动也可以上下滑动,就产生了滑动冲突。如果不进行处理,会严重影响View的效果。
下面是4种自定义View的分类,当然仁者见仁智者见智,自定义View的标准并不唯一。
1、 继承View重写onDraw方法
这种方法主要用来实现一些不规则的效果,也就是这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态的显示一些不规则图案。这需要重写onDraw方法,需要自己支持wrap_content(为wrap_content指定默认值)并处理padding。其次有必要的话还需要对外提供自定义属性。下面先实现一个简单的自定义View,绘制一个红色的实心圆。
public class CircleView extends View {
private int mColor =Color.RED;
private Paint mPaint = newPaint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Contextcontext) {
super(context);
init();
}
public CircleView(Contextcontext, AttributeSet attrs) {
this(context, attrs,0);
}
public CircleView(Context context,AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a =context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
a.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode =MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize =MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode =MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize =MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode ==MeasureSpec.AT_MOST
&&heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if(widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if(heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft= getPaddingLeft();
final int paddingRight= getPaddingRight();
final int paddingTop =getPaddingTop();
final intpaddingBottom = getPaddingBottom();
int width = getWidth()- paddingLeft - paddingRight;
int height =getHeight() - paddingTop - paddingBottom;
int radius =Math.min(width, height) / 2;
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2,
radius,mPaint);
}
}
从代码的onMearsure方法中可以看出,当宽或者高的SpecMode 是MeasureSpec.AT_MOST(wrap_content),就为其指定默认大小200px。在onDraw方法中绘制的时候考虑View四周的留白,包括圆心和半径都会考虑View四周的padding。
很多情况下我们还需要指定自定义属性,遵循如下几步:
·第一步,在value目录下创建自定义属性的XML,一般文件名采用attrs开头。这里命名为attrs.xml
<?xml version="1.0"encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
文件中声明了一个自定义属性集合CircleView,这里就添加了一个属性,当然还可以继续加,不过都遵循这个格式。里面是一个格式为color的属性circle_color,格式color指的就是颜色。自定义属性还能有很多格式,比如reference(指id)、dimension(指尺寸)和int等基本数据类型。
·第二步,在View的构造方法中解析自定义属性的值并做相处相应的处理。这里我们就需要解析circle_color这个属性的值了。代码在上面的自定义View中标记为红色了。首先加载自定义属性集合"CircleView,接着解析circle_color属性,这个属性的id是R.styleable.CircleView_circle_color,Color.RED是为其设定的在使用中的默认属性, 用的时候可以修改。最后通过recycle()释放资源。
·第三步,在布局文件中使用自定义属性。
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical">
<com.ryg.myview.ui.CircleView
android:id="@+id/circleView1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="@color/light_green"/>
</LinearLayout>
这里要注意两点,如果使用自定义属性,必须在布局文件添加shemas声明如红色代码所示。这个声明中app是自定义的的属性前缀,也可以用其他名字。使用自定义属性的前缀必须和shemas声明的自定义的属性前缀相同,如黄色背景代码所示。
2、 继承特定的View(比如TextView)
这种方法比较简单,一般用于扩展某种已有的View功能。也不需要自己支持wrap_content和padding等。不再详述。
3、 继承ViewGroup派生特殊的Layout
采用这种方式要注意你要实现的是一个和LinearLayout这一层次的View,过程会很复杂。所以可以补实现他的方方面面,仅仅完成主要功能就可以了。想必你已经想到需要做哪些工作了。你需要合适的处理ViewGroup中的测量、布局两个过程,并同时处理子元素的测量和布局过程。一位百度工程师在他的github上分享过很一段完整的自定义Layout,感兴趣的可以参见https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_3/src/com/ryg/chapter_3/ui/HorizontalScrollViewEx.java
4、 继承特定的ViewGroup(比如LinearLayout)
采用这种方法不需要自己处理测量和布局的过程。一般来说方法2能实现的方法4也能实现。只不过方法二更接近底层一点。在方法1给出范例,然后参见相关View源码即可完成2和4的自定义View,不再介绍了。