高仿QQ之未读消息小球的自定义(一)

好吧,昨天,几个好友回来了,去接了好友还有去吃饭完了一下,现在才回来,好了,现在把这个给更新了哈。
题目上已经说了,自定义,那么我们就来看下怎么去自定义:

  • 1、有两个小球(一个固定圆,一个动圆)
  • 2、然后两个球之间连接(一定区域)
  • 3、固定圆的坐标我们指定,动圆坐标根据我们的鼠标点击移动或者手指触摸移动

还有要实现的效果:

  • 1、单击小球的时候绘制大球,大球可以随手势滑动,小球不动
  • 2、大球和小球之间有一定的范围,在这个范围内,大球任意移动
  • 3、超过范围,只显示大球,返回到范围内松手,恢复原来状态(未读状态)
  • 4、超过范围,只显示大球,不返回到一定的范围内,松手,消失不见(已读状态)

好了,大致的就是这么多了,首先我们先来看下贝塞尔曲线的解释,因为在这里我们中间的连接范围用到了这个知识。我也只是大概的了解了一下,因为我们用的是已经给我们提供好的函数和接口,具体的底层怎么实现的我们就不考究了。
看下图解:(这里面的那两个蓝色的斜线之间的红色是贝塞尔曲线绘制的范围
高仿QQ之未读消息小球的自定义(一)_第1张图片
好了,我们先来定义一个类继承自View:
创建画笔

        /**
         * 创建画笔
         */
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        /**
         * 设置画笔的颜色
         */
        mPaint.setColor(Color.RED);

我们这个时候先来画圆:
首先我们就来定义圆了哈,首先定义动圆和固定圆的一些用到的变量,因为我们只用把动的改成了变量,然后我们去动态的改变这些变量,然后去重绘圆,是不是可以达到了圆的运动了呢,嘿嘿:

 /**
     * 固定圆  并且初始化
     */
    private PointF mFixedCircle = new PointF(150f, 150f);

    /**
     * 固定圆的半径
     */
    float mFixedRadius = 14f;

    /**
     * 动圆  并且初始化
     */
    private PointF mDragCircle = new PointF(80f, 80f);

    /**
     * 动圆半径
     */
    float mDragRadius = 20f;

    /**
     * 动圆两个焦点的坐标
     */
    private PointF[] mDragPoints;

    /**
     * 固定圆的两个焦点坐标
     */
    private PointF[] mFixedPoints;

    /**
     * 控制焦点
     */
    private PointF mControlPoint;

在这里我们还有一个问题,就是当我们绘制的时候,因为有了状态栏的高度,我们的画布是我们状态栏之下的,为了大球和我们的手势是一起的,首先我们要去获取状态栏的高度:

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        //获取状态栏的高度(这个工具类我稍后会上传的)
        mStatusBarHeight = Utils.getStatusBarHeight(this);

    }

重写onDraw()(onDraw()里面写):

        //保持当前画布的状态
        canvas.save();
        //移动画布
        canvas.translate(0, -mStatusBarHeight);

在这里我们先把两个圆之间的距离给获取到:

 /**
     * 获取临时的固定圆的半径
     *
     * @return
     */
    private float getTempFiexdCircle() {
        //获取到两个圆心之间的距离
        float instance = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);

        //这个是在连个圆之间的实际距离和我们定义的距离之间取得最小值
        instance = Math.min(instance, farestDistance);
        //0.0f--->1.0f>>>>>1.0f---》0.0f
        float percent = instance / farestDistance;

        return evaluate(percent, mFixedRadius, mFixedRadius * 0.2);

    }

    /**
     * 估值器
     *
     * @param fraction
     * @param startValue
     * @param endValue
     * @return
     */
    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }

好了,需要获取到的资源我们基本上差不多了,我们先来画圆吧:
onDraw()方法中:

 //根据两个圆的圆心的距离获取固定圆的半径
        float distance = getTempFiexdCircle();


        //计算连接部分
        //1、获取直线与圆的焦点
        float yOffset = mFixedCircle.y - mDragCircle.y;
        float xOffset = mFixedCircle.x - mDragCircle.x;

        /**
         * 获取斜率
         */
        Double lineK = null;
        if (xOffset != 0) {
            lineK = (double) yOffset / xOffset;
        }
        //通过几何工具获取焦点坐标
        this.mFixedPoints = GeometryUtil.getIntersectionPoints(mFixedCircle, distance, lineK);
        this.mDragPoints = GeometryUtil.getIntersectionPoints(mDragCircle, mDragRadius, lineK);
        //2、获取控制点坐标
        this.mControlPoint = GeometryUtil.getMiddlePoint(mDragCircle, mFixedCircle);

//绘制动圆
  canvas.drawCircle(mDragCircle.x, mDragCircle.y, mDragRadius, mPaint);
 //画一个固定圆
                //canvas.drawCircle(150f,150f,14f,mPaint);
   canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);


 //canvas.drawCircle(150f,150f,14f,mPaint);
                canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);

                //画连接部分   这个是用的那个贝塞尔曲线绘制的连接部分
                Path path = new Path();
                //跳到某个点1
                path.moveTo(mFixedPoints[0].x, mFixedPoints[0].y);
                //画曲线 1--->2
                path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y);
                //画直线2---->3
                path.lineTo(mDragPoints[1].x, mDragPoints[1].y);
                //画曲线3---->4
                path.quadTo(mControlPoint.x, mControlPoint.y, mFixedPoints[1].x, mFixedPoints[1].y);

                path.close();
                canvas.drawPath(path, mPaint);


  //恢复画布
        canvas.restore();

现在应该可以画出了两个圆还有就是两个圆之间的范围了如图所示:
高仿QQ之未读消息小球的自定义(一)_第2张图片

到目前为止,我们只是画出了轮廓,还是没有让让根据我们的手势动起来,这里我们来实现以下,这个时候我们就要实现一下onTouch()方法了在这里我们来判断按下、移动、抬起的手势。还有就是在这里我们在移动的时候去判断是否是显示,隐藏,断开的了,代码量也不多,在这里我就不多做解释了,代码中我解释详细一下就行了。

/**
     * 重写这个方法,让小球动起来
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = 0;
        float y = 0;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //获取到按下的时候的坐标(因为我们已经把画布往上移动了状态栏的高度了,或者是我们在这里做判断)
                x = event.getRawX();
                y = event.getRawY();

                //更新动圆的坐标
                updataDragCircle(x, y);

                break;
            case MotionEvent.ACTION_MOVE:
                //移动的时候获取坐标
                x = event.getRawX();
                y = event.getRawY();
                updataDragCircle(x, y);

                //处理断开
                float distance = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);
                if (distance > farestDistance) {  //如果获取到的距离大于我们定义的最大的距离
                    isOutToRange = true;  //断开设置为true
                    invalidate();  //重绘
                }

                break;
            case MotionEvent.ACTION_UP:
                if (isOutToRange) {  //如果是断开
                    isOutToRange = false;  //设置为false
                    //处理断开
                    float d = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);
                    if (d > farestDistance) {
                        // * a、拖拽超出范围,断开-->松手-->消失
                        //松手还没有放回去
                        isDisappear = true;

                        //重绘一下
                        invalidate();
                    } else {
                        //    * b、拖拽超出范围,断开---->放回去了--->恢复
                        updataDragCircle(mFixedCircle.x, mFixedCircle.y);
                        isDisappear = false;
                    }

                } else {

                    final PointF tempDragCircle = new PointF(mDragCircle.x, mDragCircle.y);

                    //    * c、拖拽没有超出范围,断开--->恢复
                    final ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f);
                    mAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            float percent = mAnim.getAnimatedFraction();
                            PointF p = GeometryUtil.getPointByPercent(tempDragCircle, mFixedCircle, percent);
                            updataDragCircle(p.x, p.y);
                        }
                    });
                    //差之器,这个是设置弹性的
                    mAnim.setInterpolator(new OvershootInterpolator(4));
                    mAnim.setDuration(500);
                    mAnim.start();
                }


                break;
        }
        return true;
    }


    /**
     * 更新拖拽圆的圆心坐标
     *
     * @param rawX
     * @param rawY
     */
    private void updataDragCircle(float rawX, float rawY) {
        //更新的坐标
        mDragCircle.set(rawX, rawY);
        invalidate();
    }

这个时候,我们就需要改变我们onDraw()里面的方法的范围了,也就是判断
isOutToRange和isDisappear分别为true和false的了,改变如下:

 @Override
    protected void onDraw(Canvas canvas) {

        //保持当前画布的状态
        canvas.save();
        //移动画布
        canvas.translate(0, -mStatusBarHeight);


        //根据两个圆的圆心的距离获取固定圆的半径
        float distance = getTempFiexdCircle();


        //计算连接部分
        //1、获取直线与圆的焦点
        float yOffset = mFixedCircle.y - mDragCircle.y;
        float xOffset = mFixedCircle.x - mDragCircle.x;

        /**
         * 获取斜率
         */
        Double lineK = null;
        if (xOffset != 0) {
            lineK = (double) yOffset / xOffset;
        }
        //通过几何工具获取焦点坐标
        this.mFixedPoints = GeometryUtil.getIntersectionPoints(mFixedCircle, distance, lineK);
        this.mDragPoints = GeometryUtil.getIntersectionPoints(mDragCircle, mDragRadius, lineK);
        //2、获取控制点坐标
        this.mControlPoint = GeometryUtil.getMiddlePoint(mDragCircle, mFixedCircle);


        if (!isDisappear) {
            //画拖拽圆
            //canvas.drawCircle(80f,80f,20f,mPaint);

            canvas.drawCircle(mDragCircle.x, mDragCircle.y, mDragRadius, mPaint);

            if (!isOutToRange) {
                //画一个固定圆
                //canvas.drawCircle(150f,150f,14f,mPaint);
                canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);

                //画连接部分   这个是用的那个贝塞尔曲线绘制的连接部分
                Path path = new Path();
                //跳到某个点1
                path.moveTo(mFixedPoints[0].x, mFixedPoints[0].y);
                //画曲线 1--->2
                path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y);
                //画直线2---->3
                path.lineTo(mDragPoints[1].x, mDragPoints[1].y);
                //画曲线3---->4
                path.quadTo(mControlPoint.x, mControlPoint.y, mFixedPoints[1].x, mFixedPoints[1].y);

                path.close();
                canvas.drawPath(path, mPaint);
            }
        }

        //恢复
        canvas.restore();

    }

好了,我们来看下实现的动画效果:
高仿QQ之未读消息小球的自定义(一)_第3张图片

里面用到的工具类如下:

GeometryUtil.java工具类:

package com.example.viscositydemo;

import android.graphics.PointF;

/**
 * 几何图形工具
 */
public class GeometryUtil {

    /**
     * As meaning of method name.
     * 获得两点之间的距离
     * @param p0
     * @param p1
     * @return
     */
    public static float getDistanceBetween2Points(PointF p0, PointF p1) {
        float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2));
        return distance;
    }

    /**
     * Get middle point between p1 and p2.
     * 获得两点连线的中点
     * @param p1
     * @param p2
     * @return
     */
    public static PointF getMiddlePoint(PointF p1, PointF p2) {
        return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f);
    }

    /**
     * Get point between p1 and p2 by percent.
     * 根据百分比获取两点之间的某个点坐标
     * @param p1
     * @param p2
     * @param percent
     * @return
     */
    public static PointF getPointByPercent(PointF p1, PointF p2, float percent) {
        return new PointF(evaluateValue(percent, p1.x , p2.x), evaluateValue(percent, p1.y , p2.y));
    }

    /**
     * 根据分度值,计算从start到end中,fraction位置的值。fraction范围为0 -> 1
     * @param fraction
     * @param start
     * @param end
     * @return
     */
    public static float evaluateValue(float fraction, Number start, Number end){
        return start.floatValue() + (end.floatValue() - start.floatValue()) * fraction;
    }


    /**
     * Get the point of intersection between circle and line.
     * 获取 通过指定圆心,斜率为lineK的直线与圆的交点。
     * 
     * @param pMiddle The circle center point.
     * @param radius The circle radius.
     * @param lineK The slope of line which cross the pMiddle.
     * @return
     */
    public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) {
        PointF[] points = new PointF[2];

        float radian, xOffset = 0, yOffset = 0; 
        if(lineK != null){
            radian= (float) Math.atan(lineK);
            xOffset = (float) (Math.sin(radian) * radius);
            yOffset = (float) (Math.cos(radian) * radius);
        }else {
            xOffset = radius;
            yOffset = 0;
        }
        points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset);
        points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset);

        return points;
    }
}

Utils.java工具类:

package com.example.viscositydemo;

import android.content.Context;
import android.graphics.Rect;
import android.support.v4.view.MotionEventCompat;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;

public class Utils {

    public static Toast mToast;

    public static void showToast(Context mContext, String msg) {
        if (mToast == null) {
            mToast = Toast.makeText(mContext, "", Toast.LENGTH_SHORT);
        }
        mToast.setText(msg);
        mToast.show();
    }

    /**
     * dip 转换成 px
     * @param dip
     * @param context
     * @return
     */
    public static float dip2Dimension(float dip, Context context) {
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, displayMetrics);
    }
    /**
     * @param dip
     * @param context
     * @param complexUnit {@link TypedValue#COMPLEX_UNIT_DIP} {@link TypedValue#COMPLEX_UNIT_SP}}
     * @return
     */
    public static float toDimension(float dip, Context context, int complexUnit) {
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        return TypedValue.applyDimension(complexUnit, dip, displayMetrics);
    }

    /** 获取状态栏高度
     * @param v
     * @return
     */
    public static int getStatusBarHeight(View v) {
        if (v == null) {
            return 0;
        }
        Rect frame = new Rect();
        v.getWindowVisibleDisplayFrame(frame);
        return frame.top;
    }

    public static String getActionName(MotionEvent event) {
        String action = "unknow";
        switch (MotionEventCompat.getActionMasked(event)) {
        case MotionEvent.ACTION_DOWN:
            action = "ACTION_DOWN";
            break;
        case MotionEvent.ACTION_MOVE:
            action = "ACTION_MOVE";
            break;
        case MotionEvent.ACTION_UP:
            action = "ACTION_UP";
            break;
        case MotionEvent.ACTION_CANCEL:
            action = "ACTION_CANCEL";
            break;
        case MotionEvent.ACTION_SCROLL:
            action = "ACTION_SCROLL";
            break;
        case MotionEvent.ACTION_OUTSIDE:
            action = "ACTION_SCROLL";
            break;
        default:
            break;
        }
        return action;
    }
}

在这里有点急促了,不过,我想这大家看了之前的几篇自定义的view,这里的应该会有点理解了哈,如果有疑问和想交流的话,可以QQ:1069584784
该自定义viewgithub项目地址:https://github.com/wuyinlei/ViscosityView
首先在这里声明一下:(这个只是简单的实现了想要的结果,但是要移植到项目中真正的使用,还是需要自己去下一点功夫的哈,我这两天也在想怎么移植到高仿QQ项目中,实现之后会有更新的哈,高仿QQ6.0项目地址:https://github.com/wuyinlei/QQ6.0)

你可能感兴趣的:(android开发,学习心得,高仿QQ6.0之自定义控件学习)