纯属好奇心驱动写的一个学习性Demo,效果如下:

这个小功能最重要的点在于起始点和触摸点之间的连接线绘制,它并不是一条单纯的直线,而是中间细两头粗的一条不规则的Path,而这个中间向内弯曲的效果正是一条贝塞尔曲线,中间这个Path是由两条贝塞尔曲线和两条直线组成。看下图:

两个带圆弧的线就是由三点确认的一个贝塞尔曲线:

在Android已经有提供画贝塞尔曲线的接口,三个点传进去,效果就出来了。
贝塞尔曲线是用三个或多个点来确定的一条曲线,它在图形图像学中有相当重要的地位,Path中也提供了一些方法来给我们模拟低阶贝赛尔曲线。
本文的主要目的是学习贝塞尔曲线,看一下贝塞尔曲线的一下使用案例。
案例一:

案例二:

案例三:

案例四:

其实还有很多,还有QQ就是上面拖拽清除消息的气泡。看了之后你会好奇是怎么做的吧。

这里不讲背景,只讲贝塞尔曲线的一些效果,先从维基百科上盗图,看看不同数量的点上绘制贝塞尔曲线的效果。
两个点,绘制出来就是直线,也就是所谓的一阶贝塞尔曲线

三个点,二阶贝塞尔曲线:

四个点,三阶贝塞尔曲线:

五个点,四阶贝塞尔曲线:

六个点,五阶贝塞尔曲线:

大家看到上面的图,其实可以这么理解,这也是参考网友的理解。不管是一阶、二阶、还是六阶,他们都至少有两个点,也就是起点和终点。先用橡皮筋把这两个点连接好。而除了起点和终点之外的点都相当于对橡皮筋有吸引力的点,他们因为距离原因的不同,对橡皮筋产生不同程度的吸力,最终达到一个平衡状态,所以橡皮筋往非起始点有所偏移,如下图:

在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,如下图:

知道了上图两个黑点的坐标,并且知道绿色线的宽度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);    }}

效果就是上面的实线效果

更多相关文章

  1. android Canvas中的clipRect、drawBitmap
  2. AndroidActivity跳转动画,让你的APP瞬间绚丽起来
  3. 【Android开源项目解析】仿支付宝付款成功及"天女散花"效果实现
  4. Android(安卓)实现书籍翻页效果----源码篇 完结篇
  5. Android开发-----03-使用Canvas绘制虚线……
  6. Android(安卓)自定义View(三):重写View实现全新控件
  7. Android(安卓)使用Toolbar+DrawerLayout快速实现仿“知乎APP”侧
  8. 实现类似Android的网格效果的列表视图
  9. Android动画特效

随机推荐

  1. Android打包签名——生成keystore到完成
  2. android 按键声音
  3. Linux 中搭建Android开发环境中碰到问题
  4. android 如何定义全局变量
  5. 【Android】ViewPager+Fragment的基本使
  6. Android通过xml文件配置数据库
  7. Android(安卓)studio更新自身或者SDK以后
  8. Android(安卓)最强工具类的使用 .blankj:
  9. Android(安卓)双开沙箱 VirtualApp 源码
  10. Android(安卓)新建工程 卡在Gradle:Resol