软件世界网 购物 网址 三丰软件 | 小说 美女秀 图库大全 游戏 笑话 | 下载 开发知识库 新闻 开发 图片素材
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
移动开发 架构设计 编程语言 Web前端 互联网
开发杂谈 系统运维 研发管理 数据库 云计算 Android开发资料
  软件世界网 -> 移动开发 -> Android贝塞尔曲线实现QQ拖拽清除效果 -> 正文阅读

[移动开发]Android贝塞尔曲线实现QQ拖拽清除效果


纯属好奇心驱动写的一个学习性Demo,效果如下:
[img]http://img.blog.csdn.net/20160403152742242
这个小功能最重要的点在于起始点和触摸点之间的连接线绘制,它并不是一条单纯的直线,而是中间细两头粗的一条不规则的Path,而这个中间向内弯曲的效果正是一条贝塞尔曲线,中间这个Path是由两条贝塞尔曲线和两条直线组成。看下图:
[img]http://img.blog.csdn.net/20160403153716793
两个带圆弧的线就是由三点确认的一个贝塞尔曲线:
[img]http://img.blog.csdn.net/20160403154141779
在Android已经有提供画贝塞尔曲线的接口,三个点传进去,效果就出来了。
贝塞尔曲线是用三个或多个点来确定的一条曲线,它在图形图像学中有相当重要的地位,Path中也提供了一些方法来给我们模拟低阶贝赛尔曲线。
本文的主要目的是学习贝塞尔曲线,看一下贝塞尔曲线的一下使用案例。
案例一:
[img]http://img.blog.csdn.net/20160403154500061
案例二:
[img]http://img.blog.csdn.net/20160403154552718
案例三:
[img]http://img.blog.csdn.net/20160403154743265
案例四:
[img]http://img.blog.csdn.net/20160403161649089
其实还有很多,还有QQ就是上面拖拽清除消息的气泡。看了之后你会好奇是怎么做的吧。
这里不讲背景,只讲贝塞尔曲线的一些效果,先从维基百科上盗图,看看不同数量的点上绘制贝塞尔曲线的效果。
两个点,绘制出来就是直线,也就是所谓的一阶贝塞尔曲线
[img]http://img.blog.csdn.net/20160403155553378
三个点,二阶贝塞尔曲线:
[img]http://img.blog.csdn.net/20160403155628003
四个点,三阶贝塞尔曲线:
[img]http://img.blog.csdn.net/20160403155646847
五个点,四阶贝塞尔曲线:
[img]http://img.blog.csdn.net/20160403155711519
六个点,五阶贝塞尔曲线:
[img]http://img.blog.csdn.net/20160403155731535
大家看到上面的图,其实可以这么理解,这也是参考网友的理解。不管是一阶、二阶、还是六阶,他们都至少有两个点,也就是起点和终点。先用橡皮筋把这两个点连接好。而除了起点和终点之外的点都相当于对橡皮筋有吸引力的点,他们因为距离原因的不同,对橡皮筋产生不同程度的吸力,所以橡皮筋往非起始点偏移,如下图:
[img]http://img.blog.csdn.net/20160403164143536
在Android中提供了绘制一阶、二阶、三阶的接口:
一阶接口:
public void lineTo(float x, float y)

二阶接口:
public void quadTo(float x1, float y1, float x2, float y2)

三阶接口:
public void cubicTo(float x1, float y1, float x2, float y2,
                        float x3, float y3)

关于最上面的QQ拖拽清除效果,就是用的二阶绘制的。
下面来简要讲解一下它的实现原理吧:
第一:那个99+消息是一个ImageView,它是根据OnTouch事件移动的,这个好理解。这是是通过在onTouch方法里面不断的setX()、setY()实现的。
第二:记录起始位置p1(x1,y1),记录手指拖拽位置p2(x2,y2),有了这两点我们就可以绘制直线了,但是我们这里绘制的是带有贝塞尔曲线的Path,所以重要工作是构造Path
第三:构造Path,如下图:
[img]http://img.blog.csdn.net/20160403171733862
知道了上图两个黑点的坐标,并且知道绿色线的宽度Len,很好求出P1、P2、P3当中的点了,画个坐标系,用三角函数就求出来了。当然这个宽度Len是根据两个黑点之间的距离动态的去计算了,它与两个黑点之间的距离成反比。构造Path的步骤是:
p1-p3-p2之间:贝塞尔曲线
p2-p5之间:直线
p5-p3-p4之间:贝塞尔曲线
p4-p1之间:直线
构造了这样一个Path,然后用实心的画笔就画出效果来了。
当然,要完全达到要求还要加上逻辑控制了,代码如下:
package rander.com.bezier.bezier;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.AnimationDrawable;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Toast;

import rander.com.bezier.R;

/**
 * Created by Rander.C on 16-4-3.
 */
public class QQDragtoClearView extends FrameLayout {
    /**
     * 最大拖拽长度
     */
    private final int DRAG_MAX_LEN = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics());
    /**
     * 默认的红点半径大小
     */
    private final int DEFAULT_RADIUS = 40;
    /**
     * 消息数量背景,QQ里面起始用的就是一个图片,而不是在红色背景上画一个数量
     */
    private ImageView mIvMsgCountBg;
    /**
     * 拖拽清除的时候的动画视图
     */
    private ImageView mIvClearAnim;

    /**
     * 手触摸的x,y坐标
     */
    private float mTouchX;
    private float mTouchY;
    /**
     * 初始的x,y坐标,默认安放的位置
     */
    private float mStartX = 300;
    private float mStartY = 300;
    /**
     * 手触摸的坐标和初始点坐标的中间位置
     * mCenterX = (mTouchX + mStartX)/2
     * mCenterY = (mTouchY + mStartY )/2
     * 这个是用来画贝塞尔曲线的一个中转点
     */
    private float mCenterX;
    private float mCenterY;
    private static final int TOUCH_SLOP = 10;

    /**
     * 画贝塞尔曲线的画笔
     */
    private Paint mPaint;
    /**
     * 画贝塞尔曲线的Path
     */
    private Path mPath;

    /**
     * 是否被点中了
     */
    private boolean isTouch;

    /**
     * 默认的半径大小
     */
    private int mRaduis = DEFAULT_RADIUS;

    /**
     * 判断贝塞尔曲线是否断掉了
     */
    private boolean isBroken = false;

    public QQDragtoClearView(Context context) {
        this(context, null);
    }

    public QQDragtoClearView(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public QQDragtoClearView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        mPath = new Path();

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setStrokeWidth(2);
        mPaint.setColor(Color.RED);

        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(60, 60);
        mIvClearAnim = new ImageView(getContext());
        mIvClearAnim.setLayoutParams(params);
        mIvClearAnim.setImageResource(R.drawable.tip_anim);
        mIvClearAnim.setVisibility(INVISIBLE);

        mIvMsgCountBg = new ImageView(getContext());
        mIvMsgCountBg.setLayoutParams(params);
        mIvMsgCountBg.setImageResource(R.drawable.skin_tips_newmessage_ninetynine);
        mIvMsgCountBg.setVisibility(VISIBLE);

        addView(mIvClearAnim);
        addView(mIvMsgCountBg);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mTouchX = (int) event.getX();
        mTouchY = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //ACTION_DOWN的作用就是判断触摸的点
                Rect rect = new Rect();
                int[] location = new int[2];
                mIvMsgCountBg.getDrawingRect(rect);
                mIvMsgCountBg.getLocationOnScreen(location);
                rect.left = location[0];
                rect.top = location[1];
                rect.right = rect.right + location[0];
                rect.bottom = rect.bottom + location[1];
                if (rect.contains((int) event.getRawX(), (int) event.getRawY())) {
                    //如果isTouch为ture则表示开始绘制开始
                    isTouch = true;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //当抬起的时候,先判断是否断开了,才会出发动画,并且消息数量提示消失
                //如果没有断开,则回到原点
                if (isBroken) {
                    boolean isPositionNoChanged = false;
                    //如果弹起位置跟起始位置相差不大,则回到起始位置,不消失
                    if (Math.abs(mTouchX - mStartX) < TOUCH_SLOP && Math.abs(mTouchY - mStartY) < TOUCH_SLOP) {
                        isPositionNoChanged = true;
                    }

                    if (isPositionNoChanged) {
                        break;
                    } else {
                        mIvMsgCountBg.setVisibility(INVISIBLE);
                        mIvClearAnim.setVisibility(View.VISIBLE);
                        mIvClearAnim.setX(mTouchX - mIvClearAnim.getWidth() / 2);
                        mIvClearAnim.setY(mTouchY - mIvClearAnim.getHeight() / 2);
                        mIvClearAnim.setImageResource(R.drawable.tip_anim);

                        ((AnimationDrawable) mIvClearAnim.getDrawable()).stop();
                        ((AnimationDrawable) mIvClearAnim.getDrawable()).start();
                        mPath.reset();

                        new Handler().postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                mIvMsgCountBg.setVisibility(VISIBLE);
                            }
                        }, 1200);
                    }
                    isBroken = false;
                }
                isTouch = false;
                mIvMsgCountBg.setX(mStartX - mIvMsgCountBg.getWidth() / 2);
                mIvMsgCountBg.setY(mStartY - mIvMsgCountBg.getHeight() / 2);
                break;
        }

        mCenterX = (mTouchX + mStartX) / 2;
        mCenterY = (mTouchY + mStartY) / 2;

        if (isTouch) {
            mIvMsgCountBg.setX(mTouchX - mIvMsgCountBg.getWidth() / 2);
            mIvMsgCountBg.setY(mTouchY - mIvMsgCountBg.getHeight() / 2);
        }
        invalidateView();
        return true;
    }

    public void invalidateView() {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            invalidate();
        } else {
            postInvalidate();
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mIvClearAnim.setX(mStartX);
        mIvClearAnim.setY(mStartY);
        mIvMsgCountBg.setX(mStartX - mIvMsgCountBg.getWidth() / 2);
        mIvMsgCountBg.setY(mStartY - mIvMsgCountBg.getHeight() / 2);
        super.onLayout(changed, l, t, r, b);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //如果在touch中,且线没有断掉,则继续绘制,会泽清楚画布.
        if (isTouch && !isBroken) {
            checkDragLen();
            canvas.drawPath(mPath, mPaint);
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
            canvas.drawCircle(mStartX, mStartY, mRaduis, mPaint);
            canvas.drawCircle(mTouchX, mTouchY, mRaduis, mPaint);
        } else {
            //相当于清楚绘制信息
            canvas.drawCircle(mStartX, mStartY, 0, mPaint);
            canvas.drawCircle(mTouchX, mTouchY, 0, mPaint);
            canvas.drawLine(0, 0, 0, 0, mPaint);
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
        }
    }

    /**
     * 检查拖拽长度是不是够了,超过一定长度就断开,不绘制了
     */
    private void checkDragLen() {
        int len = (int) Math.sqrt(Math.pow(mTouchX - mStartX, 2) + Math.pow(mTouchY - mStartY, 2));
        mRaduis = -len / 15 + DEFAULT_RADIUS;
        //如果长度超过最大长度,isBroken设置为true,在后续就不绘制path了
        if (len > DRAG_MAX_LEN) {
            isBroken = true;
            return;
        }

        //得到绘制贝塞尔曲线需要的四个点
        float offsetX = (float) (mRaduis * Math.sin(Math.atan((mTouchY - mStartY) / (mTouchX - mStartX))));
        float offsetY = (float) (mRaduis * Math.cos(Math.atan((mTouchY - mStartY) / (mTouchX - mStartX))));

        float x1 = mStartX - offsetX;
        float y1 = mStartY + offsetY;

        float x2 = mTouchX - offsetX;
        float y2 = mTouchY + offsetY;

        float x3 = mTouchX + offsetX;
        float y3 = mTouchY - offsetY;

        float x4 = mStartX + offsetX;
        float y4 = mStartY - offsetY;

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

使用activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <rander.com.bezier.bezier.QQDragtoClearView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent" />
</RelativeLayout>

Activity
package rander.com.bezier;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

效果就是上面的实线效果
......显示全文...
    点击查看全文


上一篇文章      下一篇文章      查看所有文章
2016-04-03 20:41:06  
移动开发 最新文章
深入了解android中的消息机制Handler
Android
Libgdx之BitmapFont字体
AndroidApp发布到应用市场的流程
Android开发找工作之前先看看这些知识点吧
View的事件分发机制解析
简单介绍了解白鹭引擎Egret
Cocos2d
android获取本地图片(二)
动画特效七:碰撞动画
360图书馆 软件开发资料 文字转语音 购物精选 软件下载 美食菜谱 新闻资讯 电影视频 小游戏 Chinese Culture 股票 租车
生肖星座 三丰软件 视频 开发 短信 中国文化 网文精选 搜图网 美图 阅读网 多播 租车 短信 看图 日历 万年历 2018年1日历
2018-1-16 21:03:54
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  软件世界网 --