自定义view以及事件处理

先看效果图
按住抬起

自定义view其实完全可以集成自view,viewgroup,或者现有的view。

    public JumpCircleView(Context context) {
        this(context, null);//可以直接new
    }

    public JumpCircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);//可以在xml文件中使用
    }

注释的很清楚了

这里还有个构造方法,是3个参数的,可以使用自定义的属性


     public JumpCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.JumpCircleView, defStyleAttr, R.style.AppTheme);

        custom_size = a.getDimensionPixelSize(R.styleable.JumpCircleView_size, SIZE);
        custom_background = a.getColor(R.styleable.JumpCircleView_background_color, DEFAULT_COLOR);

        a.recycle();

        init();
    }

第三个构造函数比第二个构造函数多了一个int型的值,名字叫defStyleAttr,从名称上判断,这是一个关于自定义属性的参数,实际上我们的猜测也是正确的,第三个构造函数不会被系统默认调用,而是需要我们自己去显式调用,比如在第二个构造函数里调用调用第三个函数,并将第三个参数设为0。

onMeasure–>onLayout–>onDraw
具体实行过程

在Android里,一个view的绘制流程包括:Measure,Layout和Draw,通过onMeasure知道一个view要占界面的大小,然后通过onLayout知道这个控件应该放在哪个位置,最后通过onDraw方法将这个控件绘制出来,然后才能展现在用户面前,下面我将挨个分析一下这三个方法的作用.

  • onMeasure 测量,通过测量知道一个一个view要占的大小,方法参数是两个int型的值,我们都知道,在java中,int型由4个字节(32bit)组成,在MeasureSpce中,用前两位表示mode,用后30位表示size
     int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int measuredHeight, measuredWidth;
        if (widthMode == MeasureSpec.EXACTLY) {
            measuredWidth = widthSize;
        } else {
            measuredWidth = SIZE;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            measuredHeight = heightSize;
        } else {
            measuredHeight = SIZE;
        }

        setMeasuredDimension(measuredWidth, measuredHeight);
MeasureSpce的mode有三种:EXACTLY, AT_MOST,UNSPECIFIED,除却UNSPECIFIED不谈,其他两种mode:当父布局是EXACTLY时,子控件确定大小或者match_parent,mode都是EXACTLY,子控件是wrap_content时,mode为AT_MOST;当父布局是AT_MOST时,子控件确定大小,mode为EXACTLY,子控件wrap_content或者match_parent时,mode为AT_MOST。所以在确定控件大小时,需要判断MeasureSpec的mode,不能直接用MeasureSpec的size。在进行一些逻辑处理以后,调用setMeasureDimension()方法,将测量得到的宽高传进去供layout使用。
需要明白的一点是 ,测量所得的宽高不一定是最后展示的宽高,最后宽高确定是在onLayout方法里,layou(left,top,right,bottom),不过一般都是一样的。
  • onLayout 实际上,我在自定义SketchView的时候是没有重写onLayout方法的,因为SketchView只是一个单纯的view,它不是一个view容器,没有子view,而onLayout方法里主要是具体摆放子view的位置,水平摆放或者垂直摆放,所以在单纯的自定义view是不需要重写onLayout方法,不过需要注意的一点是,子view的margin属性是否生效就要看parent是否在自身的onLayout方法进行处理,而view得padding属性是在onDraw方法中生效的。

  • onDraw 终于说到了重头戏,一般自定义控件耗费心思最多的就是这个方法了,需要在这个方法里,用Paint在Canvas上画出你想要的图案,这样一个自定义view才算结束。下面会详细讲如何在画布上画出自己想要的图案。

关于onDraw方法,在补充一句,如果是直接继承的View,那么在重写onDraw的方法是时候完全可以把super.ondraw(canvas)删掉,因为它的默认实现是空。其实任何时候都应该去点进入看一下super()方法是否是空实现。例如如果继承自button,那么ondraw方法的super()方法就不能删除。

      @Override
    protected void onDraw(Canvas canvas) {
    //super()前面是绘制想要的效果
        if(mProgressEnable){

            Drawable drawable = new ColorDrawable(Color.BLUE);
            int left = 0;
            int top = 0;
            int right = (int) (mProgress * 1.0f / mMax * getMeasuredWidth() + .5f);
            int bottom = getBottom();
            drawable.setBounds(left, top, right, bottom);// 必须的.告知绘制的范围
            drawable.draw(canvas);
        }


        super.onDraw(canvas);// 绘制文本,还会绘制背景
    }
得到一个正方形

在日常开发中,我们偶尔会需要一个正方形的imageView,一般都是通过指定宽高,但是当宽高不确定时,我们就只能寄希望于Android原声支持定义view的比例,但是现实是残酷的,系统好像是没有提供类似属性的,所以我们就只能自己去实现,其实自己写起来也特别的简单,只需要改一个参数就OK了,

    @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, widthMeasureSpec);
   }

不仔细观察是看不出来其中的奥妙的,虽然这里复写了view的onMeasure,但是貌似没有做任何处理,直接调用了super方法,但是仔细观察的话就会发现,在调用super方法的时候,第二个参数变了,本来应该是heightMeasureSpec却换成了widthMeasureSpec,这样view的高度就是view的宽度,一个SquareView就实现了,甚至如果通过自定义属性实现一个自定义比例view。

自定义属性

     <!--自定义view蹦跳-->
    <declare-styleable name="JumpCircleView">
        <attr name="background_color" format="color" />
        <attr name="size" format="dimension" />
    </declare-styleable>

使用

     <com.marc.chatpicture.widget.JumpCircleView
                android:id="@+id/sketch_view"
                app:size="24dp"
                app:background_color="@color/colorPrimary"
                android:layout_width="200dp"
                android:layout_height="200dp"
                android:layout_gravity="center" />

如果想真正的使用,别忘了:

     public JumpCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.JumpCircleView, defStyleAttr, R.style.AppTheme);

        custom_size = a.getDimensionPixelSize(R.styleable.JumpCircleView_size, SIZE);
        custom_background = a.getColor(R.styleable.JumpCircleView_background_color, DEFAULT_COLOR);

        a.recycle();

        init();
    }

下面贴出来一个具体的例子:

    public class JumpCircleView extends View {


    private int custom_size;
    private int custom_background;

    private Paint mPaint;
    private int mHeight;
    private int mWidth;
    private float scale = 1f;

    private final int SIZE = 15;//默认大小
    private final int DEFAULT_COLOR = Color.BLUE;//默认球的颜色

    public JumpCircleView(Context context) {
        this(context, null);//可以直接new
    }

    public JumpCircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);//可以在xml文件中使用
    }

    public JumpCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.JumpCircleView, defStyleAttr, R.style.AppTheme);

        custom_size = a.getDimensionPixelSize(R.styleable.JumpCircleView_size, SIZE);
        custom_background = a.getColor(R.styleable.JumpCircleView_background_color, DEFAULT_COLOR);

        a.recycle();

        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setColor(custom_background);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int measuredHeight, measuredWidth;
        if (widthMode == MeasureSpec.EXACTLY) {
            measuredWidth = widthSize;
        } else {
            measuredWidth = SIZE;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            measuredHeight = heightSize;
        } else {
            measuredHeight = SIZE;
        }

        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        mHeight = getHeight();
        mWidth = getWidth();
    }

    private ValueAnimator mAnimator;

    @Override
    protected void onDraw(Canvas canvas) {
        /*参数
        * 圆心x  圆心y   半径(这里要一直改变的) 画笔
        * */
        canvas.drawCircle(mWidth / 2, mHeight / 2, custom_size * scale, mPaint);
    }

    public void startAnimation() {
        mAnimator = ValueAnimator.ofFloat(1, 2);//从1-2不断变化
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //拿到每时每刻变化的值
                scale = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });

        // 重复次数 -1表示无限循环
        mAnimator.setRepeatCount(-1);

        // 重复模式, RESTART: 重新开始 REVERSE:恢复初始状态再开始
        mAnimator.setRepeatMode(ValueAnimator.REVERSE);

        mAnimator.start();
    }

    public void stopAnimation() {
        if (mAnimator != null) {
//            mAnimator.end();
            mAnimator.cancel();
        }
    }

//    @Override
//    public boolean onTouchEvent(MotionEvent event) {
//        switch (event.getAction()) {
//            case MotionEvent.ACTION_DOWN:
//                mAnimator.end();
                int scale = (int) mAnimator.getAnimatedValue();
//            case MotionEvent.ACTION_MOVE:
//                break;
//            case MotionEvent.ACTION_UP:
                startAnimation();
//                mAnimator.start();
//                break;
//        }
//        return true;
//    }


    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        //从视图移除
        // 关闭动画
        mAnimator.end();
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        return super.onSaveInstanceState();
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
    }
}
使用
       jumpView.startAnimation();
        jumpView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        jumpView.stopAnimation();
                    case MotionEvent.ACTION_MOVE:
                        break;
                    case MotionEvent.ACTION_UP:
                        jumpView.startAnimation();
                        break;
                }
                return true;
            }
        });

注释已经非常清楚了 ,这里要注意的是,可以在view中 重写onTouchEvetn,也可以在调用的地方写。但是:如果要监听手势,一般会想到onTouch,但是我们发现View的onTouch事件只是相应action-down。原因:onTouch中return false。解决办法:1, return true2、在xml布局里加上 Android:longClickable=”true”这两者并不完全是等价的。return true就意味着该view会继续处理抬起事件,而不会将此时间传递给父View———这也意味着只有一个view可以继续监听touch事件因此必须注意ontouch事件的传递过程。由子view传给父view,如果return false,可以传给父view,true则不能。手势最好在activity中的dispatchTouchEvent中做,这个是无法被子view屏蔽的。另外onTouch如果返回true,则不能监听onClick

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值