刷新头的升级版,仿QQ的红色消息小球

上一篇博客写了水滴状的下拉刷新头,不过那个只能垂直下拉,也就是一个方向,而且经常用QQ的人也知道,QQ消息来了后,列表右边会有一个红色消息小球,这个小球是可以拉动的,拉起来就像一根皮筋一样,效果很棒,于是我根据那个上篇博客,进行修改,得到一个可以平面拉动的小球,基本近似QQ。



嗯,效果还不错,基本原理和上篇一样,不过因为可以平面拖动,和原来相比,就相当于一维到二维进步,计算难度和运算量基本上了几个数量级,还总是要考虑圆心连线的方向性,头都晕了,数学不好可以绕过了偷笑




连接两个圆的圆心,过一个圆的圆心做垂线,交圆上AB两点,同理在大圆上面也作出CD两点

分别连线,现在要根据两个圆的圆心坐标和两个圆的半径求出ABCD这4点坐标


一开始想用解析几何的方式求4点坐标,做法:

求出圆心连线的斜率,根据垂直直线斜率相乘等于-1,得到AB直线的斜率

AB直线过原点,则AB直线方程可以用点斜式表达出,与圆的方程联解,即可得到AB两点

问题有两个:1,需要考虑到斜率不存在的情况2,需要求解二元二次方程组,很麻烦


于是,想到了三角函数的办法,因为计算机中求取反三角很简单,所以可以完全不顾解析几何那一套,做法:

求出圆心连线的斜率,根据垂直直线斜率相乘等于-1,得到AB直线的斜率

根据斜率k=tanθ,θ是直线AB和X轴的夹角,θ=arctank

AB点都是圆上的点,圆的方程已经说的很清楚了,x=cosθ*R+Rx,y=sinθ*R+Ry,其中Rx,Ry都是圆心坐标

CD点同理可得,下面需要根据这四个点做二次曲线,因此需要借助到中间点E,F。

直线EF是圆心连线的垂直平分线,交A,B两点延生的两条平行于圆心连线于E,F

因此这两个点也可以通过三角函数来求,以A为原点,已知AF的距离(圆心距一半),AF和X轴的夹角(θ+90),很容易求得

于是乎,剩下两个半圆弧的绘制方法,依旧是用那个方法、arcTo,依旧是要注意圆弧的方向,和整条路径的方向,不然很可能无法闭合曲线。


当然,实际情况比这还要复杂的多

你需要考虑这种情况


当圆出现在下面时,AB直线的斜率依旧不变,可是绘制圆弧的方向却完全不一样了,因此需要在代码中加入一个判断

闭合曲线也是麻烦的要死,嗯,说到这里,绘制这部分算是完了


至于触摸事件,摸着改动大圆圆心坐标就是了,然后刷新view


说说回弹,这次用的回弹和上次有所不同,上次用的是匀速返回,这种效果并不是很好

想作出那种皮筋,或者弹簧的效果来,果然,想到弹簧上的小球正好就是物理上面所说的简谐运动

这正是我想要的,于是乎,看原理:

我需要的是小球运动时圆心距的改变,从圆心距推算出大圆的圆心坐标

而我们可以让圆心距改变的大小是一个简谐运动,圆心距x=A*sin(w*t+Ψ

振幅A不停减少,一直到0停止,t的经历的时间(不是间隔时间)
根据圆心距和角度angle,可及时的计算出运动中的圆心坐标
small球在上面时,即islow=1,x为正,在下面时islow=-1,x为负,需要根据这个计算初相φ,φ=islow*π/2
A=手放开的时候的圆心距
周期T=2π/ω,则ω=2π/T

当A递减到0,回弹线程结束


好了,下面贴代码:

package com.example.kaifa.myapplication;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
 * TODO: document your custom view class.
 */
public class DanqiuView extends View {


    private Paint mPaint;
    /**
     * 拉伸进度
     */
    private float progress = 0;
    /**
     * view的宽高
     */
    private int viewheight, viewwdith;

    /**
     * 大圆半径
     */
    private float GreatCircleRadius = 20;
    /**
     * 小圆半径
     */
    private float SmallCircleRadius = 20;
    /**
     * 大圆的圆心
     */
    private Point GreatCirclePoint;
    /**
     * 小圆的圆心
     */
    private Point SmallCirclePoint;
    /**
     * 分别在大圆和小圆上面的4个点
     */
    private Point A, B, C, D;
    /**
     * AB直线与X轴的夹角(AB直线和圆心连线垂直),单位 弧度
     */
    private double angle;
    /**
     * 手指按下接触到的第一个点
     */
    private Point firstPoint;
    /**
     * 两条二次曲线的分别两个中间点
     */
    private Point ACmiddlePoint, BDmiddlePoint;
    /**
     * 绘制路径
     */
    private Path mPath;
    /**
     * 1表示小圆在大圆下面,-1反之
     */
    private int islow = -1;
    /**
     * 两园的圆心距
     */
    double dance=0;
    /**
     * 周期,单位/毫秒
     */
    float T=2000;

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

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

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

    private void init(AttributeSet attrs, int defStyle) {
        // Load attributes
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(0xff0000ff);
        mPaint.setStyle(Paint.Style.FILL);
        mPath = new Path();

        GreatCirclePoint = new Point();
        SmallCirclePoint = new Point();
        A = new Point();
        B = new Point();
        C = new Point();
        D = new Point();
        firstPoint = new Point();

        ACmiddlePoint = new Point();
        BDmiddlePoint = new Point();


    }

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

        GreatCirclePoint.x = w / 2;
        GreatCirclePoint.y = h / 2;

        SmallCirclePoint.x = GreatCirclePoint.x;
        SmallCirclePoint.y = GreatCirclePoint.y;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //路径重置
        mPath.reset();
        //若重合,则绘制初始圆
        if (GreatCirclePoint.x == SmallCirclePoint.x && GreatCirclePoint.y == SmallCirclePoint.y) {
            //初始绘制
            mPath.addCircle(GreatCirclePoint.x, GreatCirclePoint.y, GreatCircleRadius, Path.Direction.CW);
            canvas.drawPath(mPath, mPaint);
            return;
        }
        //计算四个点的坐标和角度值
        //AB直线和圆心连线垂直,斜率相乘等于-1.因此可以得到AB直线的斜率
        float kAB = (GreatCirclePoint.x - SmallCirclePoint.x) / (SmallCirclePoint.y - GreatCirclePoint.y);
        angle = Math.atan(kAB);
        Log.v("xingyun", "斜率K=" + kAB + "  角度a=" + angle);
        A.x = (float) (GreatCirclePoint.x + GreatCircleRadius * Math.cos(angle));
        A.y = (float) (GreatCirclePoint.y + GreatCircleRadius * Math.sin(angle));

        B.x = (float) (GreatCirclePoint.x + GreatCircleRadius * Math.cos(angle + Math.PI));
        B.y = (float) (GreatCirclePoint.y + GreatCircleRadius * Math.sin(angle + Math.PI));

        C.x = (float) (SmallCirclePoint.x + SmallCircleRadius * Math.cos(angle));
        C.y = (float) (SmallCirclePoint.y + SmallCircleRadius * Math.sin(angle));

        D.x = (float) (SmallCirclePoint.x + SmallCircleRadius * Math.cos(angle + Math.PI));
        D.y = (float) (SmallCirclePoint.y + SmallCircleRadius * Math.sin(angle + Math.PI));

        if (GreatCirclePoint.y < SmallCirclePoint.y) {
            islow = 1;
        } else {
            islow = -1;
        }

        dance = Math.sqrt((GreatCirclePoint.x - SmallCirclePoint.x) * (GreatCirclePoint.x - SmallCirclePoint.x) + (GreatCirclePoint.y - SmallCirclePoint.y) * (GreatCirclePoint.y - SmallCirclePoint.y)) / 2;

        progress = (float) dance * 2 / viewheight;
        SmallCircleRadius = (float) (20 * (1 - progress * 0.1));

        GreatCircleRadius = (float) (20 * (1 - progress));

        if (progress < 0.5) {
            BDmiddlePoint.x = (float) (B.x + dance * Math.cos(angle + islow * Math.PI / 2));
            BDmiddlePoint.y = (float) (B.y + dance * Math.sin(angle + islow * Math.PI / 2));

            ACmiddlePoint.x = (float) (A.x + dance * Math.cos(angle + islow * Math.PI / 2));
            ACmiddlePoint.y = (float) (A.y + dance * Math.sin(angle + islow * Math.PI / 2));
        } else {
            BDmiddlePoint.x = GreatCirclePoint.x / 2 + SmallCirclePoint.x / 2;
            BDmiddlePoint.y = GreatCirclePoint.y / 2 + SmallCirclePoint.y / 2;
            ACmiddlePoint.x = BDmiddlePoint.x;
            ACmiddlePoint.y=BDmiddlePoint.y;
        }
        mPath.arcTo(new RectF(GreatCirclePoint.x - GreatCircleRadius, GreatCirclePoint.y - GreatCircleRadius
                        , GreatCirclePoint.x + GreatCircleRadius, GreatCirclePoint.y + GreatCircleRadius),
                (float) (angle * 180 / Math.PI), islow * (-180));
        //从B点到D点,选取其中心点作为渐进点
        mPath.quadTo(BDmiddlePoint.x, BDmiddlePoint.y, D.x, D.y);

        mPath.arcTo(new RectF(SmallCirclePoint.x - SmallCircleRadius, SmallCirclePoint.y - SmallCircleRadius
                        , SmallCirclePoint.x + SmallCircleRadius, SmallCirclePoint.y + SmallCircleRadius),
                (float) (angle * 180 / Math.PI+180), islow * (-180));

        //从A点到C点
        mPath.quadTo(ACmiddlePoint.x, ACmiddlePoint.y, A.x, A.y);

        canvas.drawPath(mPath, mPaint);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstPoint.x = event.getX();
                firstPoint.y = event.getY();

                break;
            case MotionEvent.ACTION_MOVE:
                float dx = event.getX() - firstPoint.x;
                float dy = event.getY() - firstPoint.y;

                if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
                    SmallCirclePoint.x = SmallCirclePoint.x + dx;
                    SmallCirclePoint.y = SmallCirclePoint.y + dy;
                    invalidate();
                }

                firstPoint.x = event.getX();
                firstPoint.y = event.getY();

                break;

            case MotionEvent.ACTION_UP:
                //开启线程匀速返回,回弹
                new MyTread().start();
                break;

        }
        return true;
    }



    /**
     * 回弹的线程,作为简谐运动,则圆心距x=Asin(ωt+φ),振幅A不停减少,一直到0停止,t的经历的时间(不是间隔时间)
     * 根据圆心距和角度angle,可及时的计算出运动中的圆心坐标
     * small球在上面时,即islow=1,x为正,在下面时islow=-1,x为负,需要根据这个计算初相φ,φ=islow*π/2
     * A=手放开的时候的圆心距
     * 周期T=2π/ω,则ω=2π/T
     */
    class MyTread extends Thread {
        @Override
        public void run() {

            float A=(float)dance*2;
            float fai=(float)( islow*Math.PI/2);
            float w=(float)(2* Math.PI/T);
            long t=0;
            double angle1=angle;

            while (A>0){
                float x=(float)(A*Math.sin(w*t+fai));
                A=A-(float)0.5;
                t=t+10;
                Log.v("xingyun","振幅:"+A+"  X:"+x);
                SmallCirclePoint.x=GreatCirclePoint.x+x*(float)Math.cos(angle1+Math.PI/2);
                SmallCirclePoint.y=GreatCirclePoint.y+x*(float) Math.sin(angle1+Math.PI/2);

                try {
                    sleep(10);
                    postInvalidate();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }


        }
    }



    private class Point {
        float x, y;
    }


}


能力有限,只能做到这里啦~(计算量这么大幸好也没出现卡顿的现象,运气好好~)

完~

原文地址:https://www.cnblogs.com/xingyun1992/p/7286569.html