仿QQ消息气泡拖拽效果

原创 2016年05月31日 18:44:21

此次的自定义View是仿qq消息列表,消息气泡拖拽效果。

1.原理介绍:自定义view,绘制原始点圆,touch点圆,然后将两圆用贝塞尔曲线连接并填充。
这里写图片描述

2.应用WindowManager,将自定义view添加到屏幕中区,全屏布局,根据相关坐标,计算出目标view在屏幕中的坐标,并将目标view(消息气泡view)绘制到自定义view中。

3,源码解析
1)按中目标view代码操作

/**
     * 开始拖动
     * @param target
     */
    public void dragStart(View target){

        convertViewToBitmap(target);//将目标view转换成bitmap,ondraw绘制时用

        final int[] locations = new int[2];
        target.getLocationOnScreen(locations);//目标view在屏幕中的坐标

        windowManagerAddView(getContext(),this);//添加到屏幕中

        this.initPoints(locations[0],locations[1],target.getWidth(),target.getHeight());//初始化相关坐标

    }

2)开始拖动代码操作

/**
     * 用户移动view,并更新
     * @param x
     * @param y
     */
    public void updatePoints(float x,float y){
        touchX = x;
        touchY = y - getStatusBarHeight();
        centerX = startCircleX/2 +touchX/2;
        centerY = startCircleY/2 +touchY/2;

        invalidate();
    }

3)拖动抬起代码操作

/**
     * 拖动结束
     */
    public void dragFinish(){
        if (onDragFinishListener != null){
            onDragFinishListener.onDragFinish();//结束监听器
        }
        if (isArriveMaxDistance){//拖动超出最大距离
            windowManagerRemoveView();//移除自定view
        }
        else {
            startRollBackAnimation(1000);//回弹动画效果
        }

    }

4)ondraw方法解析

 @Override
    protected void onDraw(Canvas canvas) {

        calculate();//计算贝塞尔曲线
        if (!isArriveMaxDistance){
            canvas.drawCircle(startCircleX,startCircleY,startRadius,mPaint);//起始圆
            canvas.drawCircle(touchX,touchY,endRadius,mPaint);//触摸点圆
            canvas.drawPath(mPath,mPaint);//两圆之间的路径绘制

        }

        canvas.drawBitmap(mDest,touchX - mDest.getWidth()/2f,touchY - mDest.getHeight()/2f,mPaint);//目标view的绘制


    }

5)贝塞尔曲线的绘制
这里写图片描述

 /**
     * 计算贝塞尔曲线
     */
    private void calculate(){
        float distance = (float) Math.sqrt(Math.pow(touchX - startCircleX,2)+Math.pow(touchY - startCircleY,2));
        startRadius = -distance/15 +endRadius;
        if (startRadius <8){
            startRadius = 8;
        }
        if (distance > GraphicUtils.dip2px(getContext(),maxDistance)){
            isArriveMaxDistance = true;
        }else {
            //根据两个圆心算出三角函数角度
            double angle = Math.atan((touchY - startCircleY)/(touchX - startCircleX));
            float offsetX = (float) (startRadius*Math.sin(angle));
            float offsetY = (float) (startRadius*Math.cos(angle));
            float x1 = startCircleX - offsetX;
            float y1 = startCircleY + offsetY;
            float x4 = startCircleX + offsetX;
            float y4 = startCircleY - offsetY;

            offsetX = (float) (endRadius*Math.sin(angle));
            offsetY = (float) (endRadius*Math.cos(angle));
            float x2 = touchX - offsetX;
            float y2 = touchY + offsetY;
            float x3 = touchX + offsetX;
            float y3 = touchY - offsetY;

            mPath.reset();
            mPath.moveTo(x1,y1);
            mPath.quadTo(centerX,centerY,x2,y2);
            mPath.lineTo(x3,y3);
            mPath.quadTo(centerX,centerY,x4,y4);
            mPath.lineTo(x1,y1);
        }


    }

4.自定义view源代码

/**
 * TODO: document your custom view class.
 */
public class BezierView extends View {
    private OnDragFinishListener onDragFinishListener;
    private Paint mPaint;
    //绘图路径
    private Path mPath;
    //
    private float maxRadius;
    //两圆的半径差
    private float diffRadius = 2;
    //起始圆半径
    private float startRadius;
    //拖动圆的半径
    private float endRadius;
    //起始圆心 在屏幕中的坐标起点
    private float startCircleX;
    private float startCircleY;
    //触摸点 相对view
    private float touchX;
    private float touchY;
    //控制点 相对view
    private float centerX;
    private float centerY;
    //最大距离
    private int maxDistance = 100;
    //是否超出最大距离
    private boolean isArriveMaxDistance;

    private Bitmap mDest;

    public void setMaxRadius(float maxRadius) {
        this.maxRadius = maxRadius;
    }

    public void setMaxDistance(int maxDistance) {
        this.maxDistance = maxDistance;
    }

    public BezierView(Context context) {
        super(context);
        init(null, 0);
    }

    public BezierView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public BezierView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }

    private void init(AttributeSet attrs, int defStyle) {
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);
        maxRadius = GraphicUtils.dip2px(getContext(),10);
    }

    /**
     * 用户按下并初始化
     * @param startX
     * @param startY
     * @param width
     * @param height
     */
    public void  initPoints(float startX,float startY,int width,int height){

        endRadius = Math.min(width,height)/2;
        if (endRadius >maxRadius){
            endRadius = maxRadius;
        }
        startRadius = endRadius-diffRadius;
        startCircleX = startX + width/2;
        startCircleY = startY + height/2 - getStatusBarHeight();
        touchX = startCircleX;
        touchY = startCircleY;

        centerX = startCircleX/2 +touchX/2;
        centerY = startCircleY/2 +touchY/2;

        invalidate();
    }
    /**
     * 开始拖动
     * @param target
     */
    public void dragStart(View target){

        convertViewToBitmap(target);

        final int[] locations = new int[2];
        target.getLocationOnScreen(locations);

        windowManagerAddView(getContext(),this);

        this.initPoints(locations[0],locations[1],target.getWidth(),target.getHeight());

    }
    /**
     * 用户移动view,并更新
     * @param x
     * @param y
     */
    public void updatePoints(float x,float y){
        touchX = x;
        touchY = y - getStatusBarHeight();
        centerX = startCircleX/2 +touchX/2;
        centerY = startCircleY/2 +touchY/2;

        invalidate();
    }

    /**
     * 拖动结束
     */
    public void dragFinish(){
        if (onDragFinishListener != null){
            onDragFinishListener.onDragFinish();
        }
        if (isArriveMaxDistance){
            windowManagerRemoveView();
        }
        else {
            startRollBackAnimation(1000);
        }

    }
    /**
     * 将view转换成bitmap
     * @param view
     * @return
     */
    private Bitmap convertViewToBitmap(View view) {
        int width = view.getWidth();
        int height = view.getHeight();
        mDest = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
        //定义一个指定位图的画布,来绘制内容
        Canvas canvas = new Canvas(mDest);
        //将view的内容绘制到bitmap上
        view.draw(canvas);
        return mDest;
    }
    /**
     * 回滚状态动画
     */
    private void startRollBackAnimation(long duration) {
        //属性动画弹性效果 缓动函数
        ValueAnimator rollBackAnim = ObjectAnimator.ofObject(new PointEvaluator(duration),new Point(touchX,touchY),new Point(startCircleX,startCircleY));
        rollBackAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Point point = (Point) animation.getAnimatedValue();
                touchX = point.getX();
                touchY = point.getY();
                centerX = startCircleX/2 +touchX/2;
                centerY = startCircleY/2 +touchY/2;
                postInvalidate();
            }
        });

        rollBackAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                //可以自己定义监听,进行目标view的处理
                BezierView.this.clearAnimation();
                windowManagerRemoveView();
            }
        });
        rollBackAnim.setDuration(duration);
        rollBackAnim.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {

        calculate();
        if (!isArriveMaxDistance){
            canvas.drawCircle(startCircleX,startCircleY,startRadius,mPaint);
            canvas.drawCircle(touchX,touchY,endRadius,mPaint);
            canvas.drawPath(mPath,mPaint);

        }

        canvas.drawBitmap(mDest,touchX - mDest.getWidth()/2f,touchY - mDest.getHeight()/2f,mPaint);


    }

    /**
     * 计算贝塞尔曲线
     */
    private void calculate(){
        float distance = (float) Math.sqrt(Math.pow(touchX - startCircleX,2)+Math.pow(touchY - startCircleY,2));
        startRadius = -distance/15 +endRadius;
        if (startRadius <8){
            startRadius = 8;
        }
        if (distance > GraphicUtils.dip2px(getContext(),maxDistance)){
            isArriveMaxDistance = true;
        }else {
            //根据两个圆心算出三角函数角度
            double angle = Math.atan((touchY - startCircleY)/(touchX - startCircleX));
            float offsetX = (float) (startRadius*Math.sin(angle));
            float offsetY = (float) (startRadius*Math.cos(angle));
            float x1 = startCircleX - offsetX;
            float y1 = startCircleY + offsetY;
            float x4 = startCircleX + offsetX;
            float y4 = startCircleY - offsetY;

            offsetX = (float) (endRadius*Math.sin(angle));
            offsetY = (float) (endRadius*Math.cos(angle));
            float x2 = touchX - offsetX;
            float y2 = touchY + offsetY;
            float x3 = touchX + offsetX;
            float y3 = touchY - offsetY;

            mPath.reset();
            mPath.moveTo(x1,y1);
            mPath.quadTo(centerX,centerY,x2,y2);
            mPath.lineTo(x3,y3);
            mPath.quadTo(centerX,centerY,x4,y4);
            mPath.lineTo(x1,y1);
        }


    }
    /**
     * 向窗口添加view
     */
    WindowManager windowManager;
    private void windowManagerAddView(Context context,View view){
        WindowManager.LayoutParams params = new WindowManager.LayoutParams();//全屏MATCH_PARENT

        params.format = PixelFormat.TRANSLUCENT;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;//后面窗口仍然可以处理点设备事件
        windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        windowManager.addView(view, params);
    }
    //移除view
    private void windowManagerRemoveView(){
        if (windowManager != null){
            windowManager.removeView(this);
        }
    }
    /**
     * 获取状态栏高度
     * @return
     */
    public int getStatusBarHeight() {
        int result = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    /**
     * 拖动抬起监听
     * @param onDragFinishListener
     */

    public void setOnDragFinishListener(OnDragFinishListener onDragFinishListener) {
        this.onDragFinishListener = onDragFinishListener;
    }

    /**
     * 自定义监听器
     */
    public interface OnDragFinishListener{
         void onDragFinish();
    }
}

动画 缓动函数

/**
 * Created by Administrator on 2016/4/6.
 * 缓动函数
 */
public class PointEvaluator implements TypeEvaluator {
    private float mDuration;

    public PointEvaluator(float mDuration) {
        this.mDuration = mDuration;
    }

    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;

        float t = mDuration * fraction;
        float x = calculate(t,startPoint.getX(),endPoint.getX() - startPoint.getX(),mDuration);
        float y = calculate(t,startPoint.getY(),endPoint.getY() - startPoint.getY(),mDuration);
        return new Point(x,y);
    }
    public Float calculate(float t, float b, float c, float d) {

        double s=1.70158;
        float a=c;
        if (t==0)
            return b;
        if ((t/=d)==1)
            return b+c;
        float p= (float) (d*.3);
        if (a < Math.abs(c)) {
            a=c;
            s=p/4;
        }
        else
        {
            s = p/(2*Math.PI) * Math.asin (c/a);
        }
        return a*(float) Math.pow(2,-10*t) * (float) Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
    }
}

6.使用方法 mBezierViewTest 目标view

 mBezierViewTest.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {

                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        mBezierView = new BezierView(CustomViewsActivity.this);

                        if (mParent != null){
                            mParent.requestDisallowInterceptTouchEvent(true);//父view禁止滚动(scrollview,listview等)
                        }

                        mBezierView.dragStart(mBezierViewTest);

                        break;
                    case MotionEvent.ACTION_MOVE:
                        mBezierViewTest.setVisibility(View.INVISIBLE);
                        mBezierView.updatePoints(event.getRawX(),event.getRawY());
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        if (mParent != null){
                            mParent.requestDisallowInterceptTouchEvent(false);
                        }
                        mBezierViewTest.setVisibility(View.VISIBLE);
                        mBezierView.dragFinish();
                        break;
                }
                return true;
            }
        });

(十三)QQ 消息气泡

版权声明:本文为博主原创文章,未经博主允许不得转载。 本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。 一、效果二、分析我们把整个气泡做成一个自定义控件,通过实际效果,...

[Swift]iOS开发:在视图上绘制文字的3种高效方法以及如何自适应文本高度

在开始之前我想先着重介绍下 NSAttributedString NSAttributedString叫做富文本,是一种带有属性的字符串,通过它可以轻松的在一个字符串中表现出多种字体、字号、字体大...

Android使用贝塞尔线高仿QQ聊天消息气泡的拖拽效果

Android高仿QQ消息气泡拖拽效果

BezierDemo源码解析-实现qq消息气泡拖拽消失的效果

这篇文章中我们比较了DraggableFlagView和BezierDemo两个项目的区别,提到将对其中一个做源码分析,那么我们就来分析BezierDemo的源码吧,因为这个项目的源码最简单,可以更直...

高仿 QQ 未读消息拖拽气泡

  • 2017年04月28日 16:09
  • 1.86MB
  • 下载

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

这种效果已经有很多人实现了,网上相关的博文也不少。今天,我就站在巨人的肩膀上再稍微做一些优化和扩展。有写的不对的地方还望大家指正。 先来看一下效果图:                       ...

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

效果图: 分析  一 : 1、应用的地方:如未读数据的清除等 2、这个控件要实现哪些功能呢? 1)拖拽超出范围时,断开了,此时我们松手,图标消失 2)拖拽超出范围时,断开了,此时我们把图标移动回去...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:仿QQ消息气泡拖拽效果
举报原因:
原因补充:

(最多只允许输入30个字)