Android自定义view

一:简单自定义View示例


   (1):在values目录下创建自定义属性的XML,比如attrs.xml;

注意:这个文件名没有什么限制,可以随便取名字;本文是在values目录下的attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="CircleImageView">
        <attr name="circle_color" format="color" />
    </declare-styleable>

</resources>


(2)在View的构造方法中解析自定义属性的值并做相应的处理。
     本文,我们需要解析circle_color这个属性的值。首先加载自定义属性集合CircleImageView,接着解析CircleImageView属性集合中CircleImageView_circle_color属性。解析完自定义属性后,通过recycle方法来释放资源。

 

public CircleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView);
        mColor= array.getColor(R.styleable.CircleImageView_circle_color, Color.RED);
        array.recycle();
        init();
    }


(3):在布局文件中使用自定义属性
  //为了使用自定义属性,需要在布局文件中添加schemas声明

<com.labo.mvpsample.CircleImageView
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:background="#fff"
        android:padding="20dp"
        app:circle_color="#FF2553EC"
        />

(4):完整CircleImageView代码

package com.labo.mvpsample;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
* author labo
* date on 2017/8/13.
* desc
* 1.需要考虑到wrap_content模式以及padding
* 2.对外提供属性,方便设置view的ui
*/

public class CircleImageView extends View {
    private int mColor= Color.RED;//默认颜色
    private Paint mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
// 如果View是在Java代码里面new的,则调用第一个构造函数
    public CircleImageView(Context context) {
        super(context);
        init();
    }


// 如果View是在.xml里声明的,则调用第二个构造函数
    public CircleImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }
     public CircleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView);
        mColor= array.getColor(R.styleable.CircleImageView_circle_color, Color.RED);
        array.recycle();
        init();
    }

    private void init() {
        //给画笔设置颜色
     mPaint.setColor(mColor);
    }

    /**
     *直接继承View的控件,如果不在onMeasrue中对wrap_content做特殊处理,那么当外界在布局中使用   
     *wrap_content时就无法达到预期效果,就相当于使用match_parent;
     *当设置wrap_content时,我们只需要给View指定一个默认的宽/高,对于非wrap_content,我们使用系统的测量值即可   
     *wrap_content它的specMode是AT_MOST模式。 
     * setMeasuredDimension()这个方法设置View的宽/高           
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpceSize = 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(widthSpceSize,200);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = 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);

    }
}



二:简单自定义ViewGroup示例

package com.mvpdemo.view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * author labo
 * date on 2017/9/12.
 * desc  本文主要是想通过案例来完成对自定义ViewGroup的理解,具体方法处理并没有做到严谨,望见谅;
 */

public class HorizontalView extends ViewGroup {
    private int lastInterceptX;
    private int lastInterceptY;
    private int lastX;
    private int lastY;
    int currentIndex = 0;
    int childWidth = 0;
    private Scroller scroller;
    private VelocityTracker velocityTracker;

    public HorizontalView(Context context) {
        super(context);
        initView();
    }


    public HorizontalView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public HorizontalView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }


    /**
     * 对scroller和velocityTracker进行初始化
     */
    private void initView() {
        scroller = new Scroller(getContext());
        velocityTracker = VelocityTracker.obtain();
    }

    /**
     * 在onMeasure中需要对wrap_content和Padding属性进行处理。
     * 当设置为wrap_content的时候,因为没有一个固定的宽高,需要我们进行设置宽高;
     * MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode指测量模式,SpecSize指在某种测量模式下的规格大小。SpecMode有三类,
     * UNSPECIFIED:父容器不对View有任何限制,要多大给多大,一般用于系统内部,表示一种测量状态;
     * EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式;
     * AT_MOST:父容器指定了一个可用大小SpceSize,View的大小不能超过这个值。对应于LayoutParams中的wrap_content;
     * 对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure就可以确定View的测量宽/高;
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //如果没有子元素,则设置宽和高都为0,这里我们采用简化的写法,我们传递的主要是思想;
        //正常情况下,我们应该根据LayoutParams中的宽高来做相应的处理。接着根据widthMode和heightMode设置宽高;
        if (getChildCount() == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            //如果宽和高都是AT_MOST,则宽度设置为所有子元素宽度的和,高度设置为第一个子元素的高度
            //正常应该是最大子元素的高度和所有子元素的宽度加margin和Padding;
            View childOne = getChildAt(0);
            int childWidth = childOne.getMeasuredWidth();
            int childHeight = childOne.getMeasuredHeight();
            setMeasuredDimension(childWidth * getChildCount(), childHeight);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            //如果宽度是AT_MOST,则宽度为所有子元素宽度的和
            int childWidth = getChildAt(0).getMeasuredWidth();
            setMeasuredDimension(childWidth * getChildCount(), heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            //如果高度是AT_MOST,则高度为第一个子元素的高度
            setMeasuredDimension(widthSize, getChildAt(0).getMeasuredHeight());
        }

    }

    /**
     * 通过onLayout来布局子元素
     * 每一种布局方式,子元素的布局方式都是不同的;
     * 遍历所有子元素,如果子元素不是GONE,则调用子元素的layout方法将其放置到合适的位置上。
     * 这里宽高我们采用简化方式:宽度是默认累加的,没有考虑子元素的margin和padding。高度默认是第一个元素的高度;
     */
    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        int childCount = getChildCount();
        int left = 0;
        View child;
        for (int j = 0; j < childCount; j++) {
            child = getChildAt(j);
            if (child.getVisibility() != View.GONE) {
                int width = child.getMeasuredWidth();
                childWidth = width;
                child.layout(left, 0, left + width, child.getMeasuredHeight());
                left += width;
            }
        }
    }

    /**
     * 处理滑动冲突
     * 如果子元素为RecyclerView,子元素为竖直滑动,HorizontalView为水平滑动,那么我们就需要处理滑动冲突;
     * 解决办法:当事件为滑动的时候,比较滑动的水平距离和竖直距离,如果水平大于竖直,则为水平滑动,那么我们就需要在HorizontalView拦截滑动事件,调用onInterceptTouchEvent方法。
     * 有一个场景(再次触摸屏幕阻止页面滑动):如果我们向左滑动切换到下一个页面的时候,在手指释放以后,页面会弹性滑动到下一个页面。此时我们想要停止页面切换;
     * 页面在滑动中,我们按下屏幕,也就是发生down事件,我们任务要停止滑动。如果Scroller还没有执行完毕,我们需要终端Scroller;
     */

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        int currentX = (int) ev.getX();//获取事件到控件左边的距离,当前事件的位置
        int currentY = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                if (!scroller.isFinished()) {
                    scroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int distanceX = currentX - lastInterceptX;
                int distanceY = currentY - lastInterceptY;
                //如果水平滑动距离大于竖直滑动,那么拦截事件;
                if (Math.abs(distanceX) > Math.abs(distanceY)) {
                    intercept = true;
                } else {
                    intercept = false;
                }
                Log.e("===","111"+intercept);
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
        }
        lastInterceptX = currentX;
        lastInterceptY = currentY;
        //在ACTION_DOWN方法,返回了false,也就是没有拦截事件,那么就不会调用我们的onTouchEvent事件。所以我们在onTouchEvent事件中是无法获取到坐标的。
        //所以我们需要在onInterceptTouchEvent对lastX、lastY进行赋值;
        lastX = currentX;
        lastY = currentY;
        return intercept;
    }

    /**
     * 我们通过onInterceptTouchEvent来判断是否拦截事件,如果返回true表示拦截事件,那么事件交由HorizontalView自身处理。
     * 那么需要重写onTouchEvent,来处理事件;
     * 在onTouchEvent我们需要处理滑动切换页面,这里需要Scroller
     * 通常情况下,滑动超过一半页面的宽度的时候才切换页面这样用户体验是不好的。如果滑动速度很快的话,我们也可以认定为用户想要滑动到其他页面。
     * 所以我们需要在onTouchEvent中的ACTION_UP对快速滑动进行处理。在这里我们需要用到VelocityTracker
     * VelocityTracker:是用来测量滑动速度的。
     */

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将事件加入到VelocityTracker类实例中
        velocityTracker.addMovement(event);
        int currentX = (int) event.getX();
        int currentY = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!scroller.isFinished()) {
                    scroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = currentX - lastX;
                scrollBy(-deltaX, 0);
                break;
            case MotionEvent.ACTION_UP:
                int distance = getScrollX() - currentIndex * childWidth;
                //判断滑动的距离是否大于宽度的一半,如果大于一半就进行切换页面
                if (Math.abs(distance) > childWidth / 2) {
                    if (distance > 0) {
                        currentIndex++;
                    } else {
                        currentIndex--;
                    }
                } else {
                    //当滑动距离没有超过一半的时候,如果是快速滑动我们也任务是切换页面
                    //获得水平方向的速度,1000表示1000ms,表示1000ms速度的最大值
                    velocityTracker.computeCurrentVelocity(1000);
                    float xVelocity = velocityTracker.getXVelocity();
                    if (Math.abs(xVelocity) > 150) {//如果速度大于150,我们认为是快速滑动。
                        //这里,如果我们手指向左滑动,速度是小于0的,我们想要进入右面的页面;
                        if (xVelocity > 0) {
                            currentIndex--;
                        } else {
                            currentIndex++;
                        }
                    }
                }
                //对currentIndex进行赋值判断
                currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;
                //调用smoothScrollTo方法进行弹性滑动
                smoothScrollTo(currentIndex * childWidth, 0);
                //我们需要重置速度计算器
                velocityTracker.clear();
                break;
        }

        return super.onTouchEvent(event);
    }

    /**
     * scrollTo(x,y):表示移动到一个具体的坐标点
     * scrollBy(dx,dy):表示移动的增量为dx,dy,scrollBy实际上调用的是scrollTo
     * scrollTo、scrollBy移动的是View的内容,如果在ViewGroup中使用,则是移动的所有的子View;
     * 用scrollTo、scrollBy这两个方法进行滑动的时候,这个滑动过程是瞬间完成的。用户体验不是很好
     * 如果我们想是实现过度滑动的效果,我们需要使用Scroller,设置滑动的时间。
     */
    private void smoothScrollTo(int destX, int dextY) {
        scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), dextY - getScrollY(), 1000);
        invalidate();//刷新界面
    }

    /**
     * Scroller本身是不能实现View的滑动的,它需要与View的computeScroll方法结合才能实现弹性滑动的效果。
     * 在MotionEvent.ACTION_UP事件触发时调用startScroll方法->马上调用invalidate / postInvalidate方法 -> 会请求View重绘,
     * 导致View.draw方法被执行->会调用View.computeScroll方法,此方法是空实现,需要自己处理逻辑。
     * 根据时间的流逝动态计算一小段时间里View滑动的距离,并得到当前View位置,
     * 再通过scrollTo继续滑动。即把一次滑动拆分成无数次小距离滑动从而实现弹性滑动。
     *
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        //先判断computeScrollOffset,若为true(表示滚动未结束),
        // 则执行scrollTo方法,它会再次调用postInvalidate,如此反复执行,直到返回值为false。
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();//通过不断的重绘不断的调用computeScroll方法
        }
    }
}

三、自定义流式布局
 

class MyFlowLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var width = 0 //FlowLayout的宽度
        var height = 0//FlowLayout的高度
        var lineWidth = 0 //每一行的宽度
        var lineHeight = 0 //每一行的高度
        /**
         * 步骤一:获取ViewGroup的测量模式和测量大小
         */
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val measureWidth = MeasureSpec.getSize(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val measureHeight = MeasureSpec.getSize(heightMeasureSpec)
        /**
         * 步骤二:计算所有子view的宽高
         */
        //遍历所有子view
        for (i in 0 until childCount) {
            //1、获取子view
            val childView = getChildAt(i)
            //2、让子视图进行自我测量
            measureChild(childView, widthMeasureSpec, heightMeasureSpec)
            //3、获取子view的宽高
            var childWidth = childView.measuredWidth
            var childHeight = childView.measuredHeight
            //4、获取子view的margin,计算当前子view的宽高
            val childParams = childView.layoutParams
            //这里我们要重写generateLayoutParams方法。
            if (childParams is MarginLayoutParams) {
                childWidth += childParams.leftMargin + childParams.rightMargin
                childHeight += childParams.topMargin + childParams.bottomMargin
            }
            /**
             * 5、我们做的是FlowLayout流式布局,要计算换行。
             * a:(如果当前行的宽度+下一个子view的宽度)小于ViewGroup的宽度,
             *      我们将当前子控件的宽度累加到lineWidth上。
             * b:(如果当前行的宽度+下一个子view的宽度)大于ViewGroup的宽度,
             *      1、我们就需要进行换行,把当前lineWidth和lineHeight累加到ViewGroup上。
             *      2、重新初始化lineWidth和lineHeight,
             *      由于换行,那当前控件就是下一行控件的第一个控件,
             *      那么当前行的行高就是这个控件的高,当前行的行宽就是这个控件的宽度值了。
             */

            if ((lineWidth + childWidth) > measureWidth) {
                //这里需要换行,所以把当前行的宽高,叠加到ViewGroup的宽高上。
                width = Math.max(lineWidth, childWidth)
                height += lineHeight
                //注意:这里给下一行设置宽、高
                lineHeight = childHeight
                lineWidth = childWidth
            } else {//不需要换行
                //比较行高和当前子view的高,把最大高度赋值给当前行高
                lineHeight = Math.max(lineHeight, childHeight)
                lineWidth += childWidth
            }
            /**
             * 6、因为我们是在比较的时候,把每一行的宽高叠加到ViewGroup的宽高上的。
             * 所以当最后一个子view的时候,也就是最后一行。我们要把最后一行的宽高叠加到ViewGroup的宽高上。
             */
            if (i == childCount - 1) {
                width = Math.max(width, lineWidth)
                height += lineHeight
            }
        }

        /**
         * 7、最后通过setMeasuredDimension把ViewGroup的宽高设置到系统中
         * 当测量模式是MeasureSpec.EXACTILY的时候,我们就不需要计算viewgroup的大小了。
         */
        val currentWidth = if (widthMode == MeasureSpec.EXACTLY) {
            measureWidth
        } else {
            width
        }
        val currentHeight = if (heightMode == MeasureSpec.EXACTLY) {
            measureHeight
        } else {
            height
        }
        setMeasuredDimension(currentWidth, currentHeight)
    }

    /**
     * 布局所有子控件
     */
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        val viewGroupWidth = measuredWidth
        var top = 0//当前坐标的top坐标
        var left = 0
        var lineWidth = 0
        var lineHeight = 0
        //1、遍历所以子view,给每个子view布局
        for (i in 0 until childCount) {
            //2、获取当前子view
            val childView = getChildAt(i)
            //3、获取当前子view的宽高
            val childParams = childView.layoutParams
            var childWidth = childView.measuredWidth
            var childHeight = childView.measuredHeight
            //4、把当前子view的margin累加到子view的宽高上
            if (childParams is MarginLayoutParams) {
                childWidth += childParams.leftMargin + childParams.rightMargin
                childHeight += childParams.topMargin + childParams.bottomMargin
            }
            //5、判断是否换行
            if ((lineWidth + childWidth) > viewGroupWidth) {//换行
                top = lineHeight
                left = 0
                lineHeight += childHeight
                lineWidth = childWidth
            } else {//不换行
                lineHeight = Math.max(lineHeight, childHeight)
                lineWidth += childWidth
            }
            //6、计算当前子view的left、right、top、bottom
            var lc = left
            var tc = top
            if (childParams is MarginLayoutParams) {
                lc += childParams.leftMargin
                tc += childParams.topMargin
            }
            var rc = lc + childView.measuredWidth
            var bc = tc + childView.measuredHeight
            //7、给子view进行layout布局
            childView.layout(lc, tc, rc, bc)
            //8、给下一个子view设置left值
            //注意:这里不能把子view的rc设置给left,因为还需要加上子view的marginRight。
            left += childWidth
        }
    }

    /**
     * 获取到childView,通过 child.getLayoutParams()获取child对应的LayoutParams实例。
     * 将其强转成MarginLayoutParams;然后获取对应的margin值,计算childWidth时添加上左边间距和右边间距。
     */
    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }

    override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
        return MarginLayoutParams(p)
    }

    override fun generateDefaultLayoutParams(): LayoutParams {
        return MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
    }
}

四、橡皮擦


/**
 * author : Naruto
 * date   : 2020-10-04
 * desc   : 橡皮擦
 * version:
 */
class EraserView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private var mBitPaint: Paint = Paint()
    private lateinit var mBmpDST: Bitmap
    private lateinit var mBmpSRC: Bitmap
    private val mPath: Path = Path()
    private var mPreX = 0f
    private var mPreY = 0f

    init {
        setLayerType(View.LAYER_TYPE_HARDWARE, null)//禁用硬件加速
        mBitPaint.color = Color.RED
        mBitPaint.style = Paint.Style.STROKE
        mBitPaint.strokeWidth = 45f
        mBmpSRC = BitmapFactory.decodeResource(resources, R.mipmap.naruto)
        mBmpDST = Bitmap.createBitmap(mBmpSRC.width, mBmpSRC.height, Bitmap.Config.ARGB_8888)
    }

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        val layerId =
            canvas?.saveLayer(
                0f, 0f,
                width.toFloat(),
                height.toFloat(), null
            )
        //先把手指轨迹画到目标bitmap上
        val c = Canvas(mBmpDST)
        c.drawPath(mPath, mBitPaint)
        //把目标图像画到画布上
        canvas?.drawBitmap(mBmpDST, 0f, 0f, mBitPaint)

        //计算源图像区域
        mBitPaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_OUT))
        canvas?.drawBitmap(mBmpSRC, 0f, 0f, mBitPaint)

        mBitPaint.xfermode = null
        canvas?.restoreToCount(layerId!!)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                mPath.moveTo(event.x, event.y)
                mPreX = event.x
                mPreY = event.y
                return true
            }
            MotionEvent.ACTION_MOVE -> {
                val endX = (mPreX + event.x) / 2
                val endY = (mPreY + event.y) / 2
                mPath.quadTo(mPreX, mPreY, endX, endY)
                mPreX = event.x
                mPreY = event.y
            }
        }
        postInvalidate()
        return super.onTouchEvent(event)
    }

}

五、QQ拖拽效果

/**
 * author : Naruto
 * desc   :
 * 1、根据手指所在位置画一个圆
 * 2、用贝塞尔曲线,链接两个圆。
 * 3、添加TextView,显示消息数量。
 * 4、当用户点击的时候,设置textview的位置为点击时候的坐标。用户未点击的时候,显示在原来的位置。
 * 5、判断是否画圆(当用户点击的时候,根据当前用户的手指位置,是否在原来的位置内)
 */
class RedPointView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
    private var mStartPoint: PointF //起始的圆心位置
    private var mCurPoint: PointF//手指当前位置
    private var mPaint: Paint = Paint()
    private var mPath: Path
    private val DEFAULT_RADIUS = 40f
    private var mRadius = DEFAULT_RADIUS
    private var mTouch = false
    private var isAnimStart = false//表示动画效果 当true的时候,贝塞尔曲线和之前的圆应该消失。也就不绘画了

    private var mTv: TextView
    private var mImg: ImageView

    init {
        mPaint.color = Color.RED
        mPaint.style = Paint.Style.FILL
        mPath = Path()
        mStartPoint = PointF(100f, 100f)
        mCurPoint = PointF()
        val params = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
        mTv = TextView(getContext())
        mTv.layoutParams = params
        mTv.setPadding(10, 10, 10, 10)
        mTv.setTextColor(Color.WHITE)
        mTv.setTextSize(10f)
        mTv.setBackgroundResource(R.drawable.tip_anim)
        mTv.setText("99+")
        addView(mTv)
        mImg = ImageView(getContext())
        mImg.layoutParams = params
        mImg.setImageResource(R.drawable.loading_main)
        mImg.visibility = View.GONE
        addView(mImg)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        event ?: return false
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                //判断当前点击区域是否在textview内部
                val rect = Rect()
                val location = IntArray(2)
                mTv.getLocationOnScreen(location)
                rect.left = location[0]
                rect.top = location[1]
                rect.right = mTv.width + location[0]
                rect.bottom = mTv.height + location[1]
                if (rect.contains(event.rawX.toInt(), event.rawY.toInt())) {
                    mTouch = true
                }

            }
            MotionEvent.ACTION_UP -> {
                mTouch = false

                if (mRadius < 9) {
                    isAnimStart = true
                    mImg.setX(mCurPoint.x - mTv.getWidth() / 2)
                    mImg.setY(mCurPoint.y - mTv.getHeight() / 2)
                    mImg.setVisibility(View.VISIBLE)
                    val animationDrawable = mImg.getDrawable() as AnimationDrawable
                    animationDrawable.start()
                    postDelayed({
                        animationDrawable.stop()
                        mImg.visibility = View.GONE
                    }, 1000)
                    mTv.visibility = View.GONE
                } else {
                    mRadius = DEFAULT_RADIUS
                }
            }
        }
        mCurPoint.set(event.x, event.y)
        postInvalidate()
        return true
    }

    /**
     * 一、onDraw和dispatchDraw的区别
     * onDraw()的意思是绘制视图自身
     * dispatchDraw()是绘制子视图
     * 无论是View还是ViewGroup对它们俩的调用顺序都是onDraw()->dispatchDraw()

     * 但在ViewGroup中,当它有背景的时候就会调用onDraw()方法,否则就会跳过onDraw()直接调用dispatchDraw();
     * 所以如果要在ViewGroup中绘图时,往往是重写dispatchDraw()方法
     * 在View中,onDraw()和dispatchDraw()都会被调用的,所以我们无论把绘图代码放在onDraw()或者dispatchDraw()中都是可以得到效果的,
     * 但是由于dispatchDraw()的含义是绘制子控件,所以原则来上讲,在绘制View控件时,我们是重新onDraw()函数
     * 总结:在绘制View控件时,需要重写onDraw()函数,在绘制ViewGroup时,需要重写dispatchDraw()函数。
     * 二、save()、saveLayer、restore
     * restore:每当调用Restore()函数,就会把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布,并在这个画布上做画。
     * saveLayer(图层)会创建一个全新透明的bitmap,大小与指定保存的区域一致,其后的绘图操作都放在这个bitmap上进行。在绘制结束后,会直接盖在上一层的Bitmap上显示。
     */
    override fun dispatchDraw(canvas: Canvas?) {
        canvas ?: return
        if (!mTouch || isAnimStart) {
            mTv.x = mStartPoint.x - mTv.width / 2
            mTv.y = mStartPoint.y - mTv.height / 2
        } else {
            canvas.drawCircle(mStartPoint.x, mStartPoint.y, mRadius, mPaint)
            calculatePath()
            canvas.drawCircle(mCurPoint.x, mCurPoint.y, DEFAULT_RADIUS, mPaint)
            canvas.drawPath(mPath, mPaint)
            mTv.x = mCurPoint.x - mTv.width / 2
            mTv.y = mCurPoint.y - mTv.height / 2
        }
        super.dispatchDraw(canvas)
    }


    private fun calculatePath() {
        val x = mCurPoint.x
        val y = mCurPoint.y
        val startX = mStartPoint.x
        val startY = mStartPoint.y
        // 根据角度算出四边形的四个点
        val dx = x - startX
        val dy = y - startY
        val a = Math.atan(dy / dx.toDouble())
        val offsetX = mRadius * Math.sin(a)
        val offsetY = mRadius * Math.cos(a)
        val distance = Math.sqrt(
            Math.pow(
                y - startY.toDouble(),
                2.0
            ) + Math.pow(
                x - startX.toDouble(),
                2.0
            )
        ).toFloat()
        mRadius = DEFAULT_RADIUS - distance / 15
        if (mRadius < 9) {
            mRadius = 8F
        }
        // 根据角度算出四边形的四个点
        val x1 = startX + offsetX
        val y1 = startY - offsetY
        val x2 = x + offsetX
        val y2 = y - offsetY
        val x3 = x - offsetX
        val y3 = y + offsetY
        val x4 = startX - offsetX
        val y4 = startY + offsetY
        val anchorX = (startX + x) / 2
        val anchorY = (startY + y) / 2
        mPath.reset()
        mPath.moveTo(x1.toFloat(), y1.toFloat())
        mPath.quadTo(anchorX, anchorY, x2.toFloat(), y2.toFloat())
        mPath.lineTo(x3.toFloat(), y3.toFloat())
        mPath.quadTo(anchorX, anchorY, x4.toFloat(), y4.toFloat())
        mPath.lineTo(x1.toFloat(), y1.toFloat())
    }

    fun resetView() {
        mTv.visibility = View.VISIBLE
        mTouch = false
        isAnimStart = false
        mRadius = DEFAULT_RADIUS
    }

}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值