自定义View--CascadeView

为什么要自定义View?

  1. 自定义View可以大大简化布局层次,提高效率
  2. 原生控件无法满足需求的时候,自定义View就会显得非常重要
  3. 程序员掌握了非常大的自由,只要遵循一定的步骤,几乎可以完成所有你能想到的控件,当然这个过程还是有很多细节和需要注意的地方的。

自定义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;
    }
  1. 接下来最重要的异步就是重写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();
    }
  1. 在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);
    }
  1. 重写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;
    }
  1. 最后一点就是在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提示!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章