为什么要自定义View?
- 自定义View可以大大简化布局层次,提高效率
- 原生控件无法满足需求的时候,自定义View就会显得非常重要
- 程序员掌握了非常大的自由,只要遵循一定的步骤,几乎可以完成所有你能想到的控件,当然这个过程还是有很多细节和需要注意的地方的。
自定义View的步骤
这里我们做一个和前文
自定义ViewGroup–CascadeLayout类似的控件CascadeView,还是先看一下效果图
1. 继承View,构造函数
CascadeView extends View
2.然后在构造函数中进行一些初始化的操作
在使用构造函数的时候有一点需要注意的地方,如果是使用Java代码创建CascadeView,一般我们会使用CascadeView(Context context),如果相应在XML文件中使用CascadeView,我们需要使用CascadeView(Context context, AttributeSet attrs)这个构造函数,否则不会起作用的。为了防止代码的冗余,可以把一些相同的代码抽取出来,这里的示例我就不这么做了。
private Bitmap[] mPokers = new Bitmap[3];
private int[] mPokersId = new int[] {R.drawable.poker_39,R.drawable.poker_40,R.drawable.poker_48};
private int mHeight;
private int mWidth;
private int mPaddingTop;
private int mPaddingLeft;
public CascadeView(Context context) {
super(context);
}
public CascadeView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CascadeView);
mHeight = a.getDimensionPixelSize(R.styleable.CascadeView_poker_height, 0);
mWidth = a.getDimensionPixelSize(R.styleable.CascadeView_poker_width, 0);
mPaddingTop = a.getDimensionPixelSize(R.styleable.CascadeView_poker_paddingTop, 0);
mPaddingLeft = a.getDimensionPixelSize(R.styleable.CascadeView_poker_paddingLeft, 0);
for(int i=0;i<mPokers.length;i++){
mPokers[i] = drawableToBitmap(mPokersId[i],mWidth,mHeight);
}
}
这里有一个将drawable资源id转换为bitmap的函数
private Bitmap drawableToBitmap(int drawableId,int width,int height)
{
return Bitmap.createScaledBitmap(
BitmapFactory.decodeResource(getResources(), drawableId), width, height, false);
}
如果是drawable转换为Bitmap,可以使用下面的函数
private Bitmap drawableToBitmap(Drawable drawable)
{
if(drawable instanceof BitmapDrawable){
BitmapDrawable bd = (BitmapDrawable) drawable;
return bd.getBitmap();
}
int h = drawable.getIntrinsicHeight();
int w = drawable.getIntrinsicWidth();
Bitmap bitmap = Bitmap.createBitmap(w,h,Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0,0,w,h);
drawable.draw(canvas);
return bitmap;
}
- 接下来最重要的异步就是重写onDraw函数了,ondraw函数就是把东西绘制到你的屏幕上,这个函数在界面刷新的时候会频繁的刷新(例如我们在代码中调用invalidate,都会调用onDraw函数),因此,有一个非常重要的原则就是,不能再onDraw方法中进行复杂耗时的操作,如果非要做的话,可以放到异步线程里面去,不要再UI线程中。
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
canvas.save();
for(Bitmap b:mPokers){
canvas.translate(mPaddingLeft, mPaddingTop);
canvas.drawBitmap(b, 0, 0,null);
}
canvas.restore();
}
- 在XML文件中使用
<com.daven.demo.CascadeView
android:id="@+id/cascadeview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
daven:poker_height="130dp"
daven:poker_paddingTop="20dp"
daven:poker_paddingLeft="30dp"
daven:poker_width="100dp" />
好了,到了这里,程序其实就已经差不多了。
使用ClipRect优化过度绘制
但是有一点,其实我们完全可以对程序进行一个优化。我们把手机的GPU过度绘制打开,可以看到下面的图:
其中绿色和红色的部分,就是几张扑克的重叠的部分,我们完全可以使用ClipRect把这几个部分的东西剪裁掉,只保留最上面的一层。整个过程如下
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
canvas.save();
for(int i = 0; i < mPokers.length; i++){
canvas.translate(mPaddingLeft, mPaddingTop);
canvas.save();
if( i < mPokers.length -1){
canvas.clipRect(0,0,mWidth,mHeight);
canvas.clipRect( mPaddingLeft, mPaddingTop, mWidth, mHeight,Region.Op.DIFFERENCE);
}
canvas.drawBitmap(mPokers[i], 0, 0, null);
canvas.restore();
}
canvas.restore();
}
这里我们需要说明一下Region.Op的用法和区别
- DIFFERENCE:之前剪切过除去当前要剪切的区域;
- INTERSECT:当前要剪切的区域在之前剪切过内部的部分;
- UNION:当前要剪切的区域加上之前剪切过内部的部分;
- XOR:异或,当前要剪切的区域与之前剪切过的进行异或;
- REVERSE_DIFFERENCE:与DIFFERENCE相反,以当前要剪切的区域为参照物,当前要剪切的区域除去之前剪切过的区域;
- REPLACE:用当前要剪切的区域代替之前剪切过的区域。
- 没带Op参数效果与INTERSECT的效果一样,两个区域的交集。
我们可以看到那张K,被我们剪裁成这个样子了,重叠的部分完全没有了
处理View上面的点击事件onTouchEvent
到了这里,我们的View放在那里好像还只是一个摆设,我们想如果能让其相应我们的点击事件那该多好,当时view本身就是可以有监听函数的,但是如果我们需要在点击该view的某个区域进行相应该怎么办呢?
这个时候我们就需要自己写监听函数了!
1.首先我们写一个监听器的接口
private OnViewClickListener mOnClickListener;
public void setOnClickListener(OnViewClickListener e) {
mOnClickListener = e;
}
public interface OnViewClickListener {
public void OnClick(int position);
}
- 重写onTouchEvent(), 判断点击的座标是否在K那张牌上面,如果是就响应这个回调函数
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_UP:
if( (x > mPaddingLeft && x < 2 * mPaddingLeft
&& y > mPaddingTop && y < mHeight)||
(x > mPaddingLeft && x < mWidth
&& y > mPaddingTop && y < 2*mPaddingTop) ){
mOnClickListener.OnClick(0);
}
return true;
}
return true;
}
- 最后一点就是在MainActivity中使用这个监听器了,和我们使用其他的监听器一样!
CascadeView c = (CascadeView) findViewById(R.id.cascadeview);
c.setOnClickListener(new CascadeView.OnViewClickListener() {
@Override
public void OnClick(int position) {
Toast.makeText(getBaseContext(), " You clicked K", Toast.LENGTH_SHORT).show();
}
});
点击到了之后就会回调到这个函数,然后弹出Toast提示!