Android 仿QQ未读消息拖拽删除粘性控件效果

来源:互联网 发布:淘宝规蜜投诉 编辑:程序博客网 时间:2024/05/22 03:01

效果图:


分析  一 :

1、应用的地方:如未读数据的清除等

2、这个控件要实现哪些功能呢?
1)拖拽超出范围时,断开了,此时我们松手,图标消失
2)拖拽超出范围时,断开了,此时我们把图标移动回去,图标恢复原样
3)拖拽没有超出范围时,此时我们松手,图标弹回去


3、如何实现:
1)我们先画个两个静态的圆圈,一个大的,一个小的

2)绘制中间连接的部分:

3)把静态的数值变成变量
4)不断地修改变量,重绘界面,就能动起来

分析  二 :

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

还有要实现的效果:
    1、单击小球的时候绘制大球,大球可以随手势滑动,小球不动
    2、大球和小球之间有一定的范围,在这个范围内,大球任意移动
    3、超过范围,只显示大球,返回到范围内松手,恢复原来状态(未读状态)
    4、超过范围,只显示大球,不返回到一定的范围内,松手,消失不见(已读状态)
好了,大致的就是这么多了,首先我们先来看下贝塞尔曲线的解释,因为在这里我们中间的连接范围用到了这个知识。我也只是大概的了解了一下,因为我们用的是已经给我们提供好的函数和接口,具体的底层怎么实现的我们就不考究了。
看下图解:(这里面的那两个蓝色的斜线之间的红色是贝塞尔曲线绘制的范围)

首先我们要自定义粘性VIew ,ViscosityView.java

/** * 自定义粘性view */public class ViscosityView extends View {    private Paint mPaint;    //固定圆,并且初始化    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;    //获取状态栏的高度    private int mStatusBarHeight;    /**     * 是否断开     */    private boolean isOutToRange = false;    /**     * 是否消失(是否可见)     */    private boolean isDisappear = false;    /**     * 两个圆最远的距离     */    float farestDistance = 100f;    String text = "";    /**     * 设置数字     * @param num     */    public void setNumber(int num) {        text = String.valueOf(num);    }    /**     * 初始化圆的圆心坐标     * @param x     * @param y     */    public void initCenter(float x, float y) {        mDragCircle = new PointF(x, y);        mFixedCircle = new PointF(x, y);        mControlPoint = new PointF(x, y);        invalidate();    }    public void setOnDisappearListener(OnDisappearListener mListener) {        this.mListener = mListener;    }    public void setStatusBarHeight(int statusBarHeight) {        this.mStatusBarHeight = statusBarHeight;    }    public OnDisappearListener getOnDisappearListener() {        return mListener;    }    interface OnDisappearListener {        void onDisappear(PointF mDragCenter);        void onReset(boolean isOutOfRange);    }    private OnDisappearListener mListener;    /**     * 清除     */    private void disappeared() {        isDisappear = true;        invalidate();        if (mListener != null) {            mListener.onDisappear(mDragCircle);        }    }    public ViscosityView(Context context) {        this(context, null);    }    public ViscosityView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public ViscosityView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        /**         * 创建画笔         */        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);        /**         * 设置画笔的颜色         */        mPaint.setColor(Color.RED);    }    @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();    }    /**     * 获取临时的固定圆的半径     * @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);    }    /**     * 重写这个方法,让小球动起来     * @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();                isDisappear = false;                isOutToRange = false;                //更新动圆的坐标                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(); //重绘                    return false;                }                break;            case MotionEvent.ACTION_UP:                if (isOutToRange) { //如果是断开                    isOutToRange = false; //设置为false                    //处理断开                    float d = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);                    if (d > farestDistance) {                        // * a、拖拽超出范围,断开-->松手-->消失                        //松手还没有放回去                        //isDisappear = true;                        disappeared();                        //重绘一下                        invalidate();                    } else {                        //    * b、拖拽超出范围,断开---->放回去了--->恢复                        updataDragCircle(mFixedCircle.x, mFixedCircle.y);                        isDisappear = false;                        if (mListener != null)                            mListener.onReset(isOutToRange);                    }                } 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;            default:                isOutToRange = false;                break;        }        return true;    }    /**     * 更新拖拽圆的圆心坐标     * @param rawX     * @param rawY     */    private void updataDragCircle(float rawX, float rawY) {        //更新的坐标        mDragCircle.set(rawX, rawY);        invalidate();    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        //获取状态栏的高度        mStatusBarHeight = Utils.getStatusBarHeight(this);    }}
分析下这个ViscosityView.java

创建画笔:

 public ViscosityView(Context context) {        this(context, null);    }    public ViscosityView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public ViscosityView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        /**         * 创建画笔         */        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);        /**         * 设置画笔的颜色         */        mPaint.setColor(Color.RED);    }
我们这个时候先来画圆:
首先我们就来定义圆了哈,首先定义动圆和固定圆的一些用到的变量,因为我们只用把动的改成了变量,然后我们去动态的改变这些变量,然后去重绘圆,是不是可以达到了圆的运动了呢,嘿嘿:

    private Paint mPaint;    //固定圆,并且初始化    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();
现在应该可以画出了两个圆还有就是两个圆之间的范围了如图所示:


到目前为止,我们只是画出了轮廓,还是没有让让根据我们的手势动起来,这里我们来实现以下,这个时候我们就要实现一下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();                isDisappear = false;                isOutToRange = false;                //更新动圆的坐标                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(); //重绘                    return false;                }                break;            case MotionEvent.ACTION_UP:                if (isOutToRange) { //如果是断开                    isOutToRange = false; //设置为false                    //处理断开                    float d = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);                    if (d > farestDistance) {                        // * a、拖拽超出范围,断开-->松手-->消失                        //松手还没有放回去                        //isDisappear = true;                        disappeared();                        //重绘一下                        invalidate();                    } else {                        //    * b、拖拽超出范围,断开---->放回去了--->恢复                        updataDragCircle(mFixedCircle.x, mFixedCircle.y);                        isDisappear = false;                        if (mListener != null)                            mListener.onReset(isOutToRange);                    }                } 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;            default:                isOutToRange = false;                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();    }
好了,我们来看下实现的动画效果:

工具类:

GeometryUtil.Java工具类:

package vp.zyqj.zz.viscositypoint;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 vp.zyqj.zz.viscositypoint;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;}}

参考:http://blog.csdn.net/wuyinlei/article/details/50634839      http://www.jb51.net/article/108265.htm       http://blog.csdn.net/gesanri/article/details/48490873   

http://blog.csdn.net/zhangphil/article/details/49746709   http://www.bubuko.com/infodetail-1092644.html  

源码:http://download.csdn.net/detail/lijinweii/9905291