QQ的粘性控件的实现原理

声明:本文资料来自于传智播客安卓52期
先来张效果图:
这里写图片描述
话不多说,线上代码,然后逐个分析

  • 自定义粘性控件
public class GooView extends View {

    private static final String TAG = "TAG";
    private Paint mPaint;

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

    public GooView(Context context, AttributeSet attrs) {
        this(context, attrs , 0);
    }

    public GooView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // 做初始化操作

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);
    }

    PointF[] mStickPoints = new PointF[]{
            new PointF(250f, 250f),
            new PointF(250f, 350f)
    };
    PointF[] mDragPoints = new PointF[]{
            new PointF(50f, 250f),
            new PointF(50f, 350f)
    };
    PointF mControlPoint = new PointF(150f, 300f);
    PointF mDragCenter = new PointF(80f, 80f);
    float mDragRadius = 14f;
    PointF mStickCenter = new PointF(150f, 150f);
    float mStickRadius = 12f;
    private int statusBarHeight;
    float farestDistance = 80f;
    private boolean isOutofRange;//是否超出界限控制区
    private boolean isDisappear;//拖动圆是否存在

    @Override
    protected void onDraw(Canvas canvas) {

        // 计算连接点值, 控制点, 固定圆半径

            // 1. 获取固定圆半径(根据两圆圆心距离)
            float tempStickRadius = getTempStickRadius();

            // 2. 获取直线与圆的交点
            float yOffset = mStickCenter.y - mDragCenter.y;
            float xOffset = mStickCenter.x - mDragCenter.x;
            Double lineK = null;
            if(xOffset != 0){
                lineK = (double) (yOffset / xOffset);
            }
            // 通过几何图形工具获取交点坐标
            mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK);
            mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius, lineK);

            // 3. 获取控制点坐标
            mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);


        // 保存画布状态
        canvas.save();
        canvas.translate(0, -statusBarHeight);

            // 画出最大范围(参考用)
            mPaint.setStyle(Style.STROKE);
            canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, mPaint);
            mPaint.setStyle(Style.FILL);

        if(!isDisappear){
            if(!isOutofRange){
                // 3. 画连接部分
                Path path = new Path();
                    // 跳到点1
                path.moveTo(mStickPoints[0].x, mStickPoints[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, mStickPoints[1].x, mStickPoints[1].y);
                path.close();
                canvas.drawPath(path, mPaint);

                    // 画附着点(参考用)
                    mPaint.setColor(Color.BLUE);
                    canvas.drawCircle(mDragPoints[0].x, mDragPoints[0].y, 3f, mPaint);
                    canvas.drawCircle(mDragPoints[1].x, mDragPoints[1].y, 3f, mPaint);
                    canvas.drawCircle(mStickPoints[0].x, mStickPoints[0].y, 3f, mPaint);
                    canvas.drawCircle(mStickPoints[1].x, mStickPoints[1].y, 3f, mPaint);
                    mPaint.setColor(Color.RED);

                // 2. 画固定圆
                canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, mPaint);
            }

            // 1. 画拖拽圆
            canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, mPaint);
        }


        // 恢复上次的保存状态
        canvas.restore();
    }

    // 获取固定圆半径(根据两圆圆心距离)
    private float getTempStickRadius() {
        float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter);

//      if(distance> farestDistance){
//          distance = farestDistance;
//      }
        distance = Math.min(distance, farestDistance);

        // 0.0f -> 1.0f
        float percent = distance / farestDistance;
        Log.d(TAG, "percent: " + percent);

        // percent , 100% -> 20% 
        return evaluate(percent, mStickRadius, mStickRadius * 0.2f);
    }

    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x;
        float y;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isOutofRange = false;
                isDisappear = false;
                x = event.getRawX();
                y = event.getRawY();
                updateDragCenter(x, y);

                break;
            case MotionEvent.ACTION_MOVE:
                x = event.getRawX();
                y = event.getRawY();
                updateDragCenter(x, y);

                // 处理断开事件
                float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter);
                if(distance > farestDistance){
                    isOutofRange = true;
                    invalidate();
                }

                break;
            case MotionEvent.ACTION_UP:
                if(isOutofRange){
                    float d = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter);
                    if(d > farestDistance){
                        // a. 拖拽超出范围,断开, 松手, 消失
                        isDisappear = true;
                        invalidate();
                    }else {
                        //b. 拖拽超出范围,断开,放回去了,恢复
                        updateDragCenter(mStickCenter.x, mStickCenter.y);
                    }

                }else {
    //              c. 拖拽没超出范围, 松手,弹回去      
                    final PointF tempDragCenter = new PointF(mDragCenter.x, mDragCenter.y);

                    ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f);
                    mAnim.addUpdateListener(new AnimatorUpdateListener() {

                        @Override
                        public void onAnimationUpdate(ValueAnimator mAnim) {
                            // 0.0 -> 1.0f
                            float percent = mAnim.getAnimatedFraction();
                            PointF p = GeometryUtil.getPointByPercent(tempDragCenter, mStickCenter, percent);
                            updateDragCenter(p.x, p.y);
                        }
                    });
                    mAnim.setInterpolator(new OvershootInterpolator(4));
                    mAnim.setDuration(500);
                    mAnim.start();
                }

                break;

        default:
            break;
        }

        return true;
    }

    /**
     * 更新拖拽圆圆心坐标,并重绘界面
     * @param x
     * @param y
     */
    private void updateDragCenter(float x, float y) {
        mDragCenter.set(x, y);
        invalidate();
    }

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

        statusBarHeight = Utils.getStatusBarHeight(this);
    }


}
  • 几何图形工具类
**
 * 几何图形工具
 */
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;
    }
}
  • Activity
public class MainActivity extends Activity {
    private GooView  gv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        gv=new GooView(MainActivity.this)  
        setContentView(gv);
        gv.setGooViewListener(new GooViewListener(){
           //重写GooViewListener接口里面的方法    
       });

    }

}

这里写图片描述

  • GooView 详解:
    这里属于自定义View(继承于View)

    extends View (GooView extends View)
    重写构造方法
    进行测量
    重写onDraw方法

    重写构造方法并初始化一个画笔(Paint对象)

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

    public GooView(Context context, AttributeSet attrs) {
        this(context, attrs , 0);
    }

    public GooView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // 做初始化操作

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);
    }

这里写图片描述

  • 构造如上所示的静态图坐标
   //固定圆的圆上两点坐标
    PointF[] mStickPoints = new PointF[]{
            new PointF(250f, 250f),
            new PointF(250f, 350f)
    };
    //移动圆的圆上两点坐标
    PointF[] mDragPoints = new PointF[]{
            new PointF(50f, 250f),
            new PointF(50f, 350f)
    };
    //两圆圆心连线的中点坐标
    PointF mControlPoint = new PointF(150f, 300f);
    //拖动圆的圆心
    PointF mDragCenter = new PointF(80f, 80f);
    //拖动圆的半径
    float mDragRadius = 14f;
    //固定圆的圆心
    PointF mStickCenter = new PointF(150f, 150f);
    //静态圆的半径
    float mStickRadius = 12f;
    private int statusBarHeight;
    //控制区的半径
    float farestDistance = 80f;

重点
onDraw方法( 计算连接点值, 控制点, 固定圆半径)

@Override
    protected void onDraw(Canvas canvas) {
            /*1. 获取固定圆半径(根据两圆圆心距离)
            float tempStickRadius = getTempStickRadius();

            // 2. 获取直线与圆的交点
            float yOffset = mStickCenter.y - mDragCenter.y;
            float xOffset = mStickCenter.x - mDragCenter.x;
            Double lineK = null;
            if(xOffset != 0){
                lineK = (double) (yOffset / xOffset);
            }
            // 通过几何图形工具获取交点坐标
            mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK);
            mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius, lineK);

            // 3. 获取控制点坐标
            mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);


        // 保存画布状态
        canvas.save();
        canvas.translate(0, -statusBarHeight);

            // 画出最大范围(参考用)
            mPaint.setStyle(Style.STROKE);
            canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, mPaint);
            mPaint.setStyle(Style.FILL);

        if(!isDisappear){//拖拽圆需要存在
            if(!isOutofRange){//如果拖拽未超出界限
                // 3. 画连接部分
                Path path = new Path();
                    // 跳到点1
                path.moveTo(mStickPoints[0].x, mStickPoints[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, mStickPoints[1].x, mStickPoints[1].y);
                path.close();
                canvas.drawPath(path, mPaint);

                    // 画附着点(参考用)
                    mPaint.setColor(Color.BLUE);
                    canvas.drawCircle(mDragPoints[0].x, mDragPoints[0].y, 3f, mPaint);
                    canvas.drawCircle(mDragPoints[1].x, mDragPoints[1].y, 3f, mPaint);
                    canvas.drawCircle(mStickPoints[0].x, mStickPoints[0].y, 3f, mPaint);
                    canvas.drawCircle(mStickPoints[1].x, mStickPoints[1].y, 3f, mPaint);
                    mPaint.setColor(Color.RED);

                // 2. 画固定圆
                canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, mPaint);
            }

            // 1. 画拖拽圆
            canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, mPaint);
        }


        // 恢复上次的保存状态
        canvas.restore();
    }

分析:

float tempStickRadius = getTempStickRadius();
作用是根据两个圆心的距离的变化动态设置固定圆的半径,以保证在拖动过程中固定圆的大小发生变化

这里写图片描述

  • 获取直线与圆的交点:用于构建贝塞尔曲线(即上图中的P0和P2)
float yOffset = mStickCenter.y - mDragCenter.y;
            float xOffset = mStickCenter.x - mDragCenter.x;
            Double lineK = null;
            if(xOffset != 0){
                lineK = (double) (yOffset / xOffset);
            }
            // 通过几何图形工具获取交点坐标
            mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK);
            mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius, lineK);
  • 获取控制点坐标(即上图中的P1)
mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);
  • 画出界定区域圆圈
        // 保存画布状态
        canvas.save();
        canvas.translate(0, -statusBarHeight);

            // 画出最大范围(参考用)即控制界限区域的那个圆
            mPaint.setStyle(Style.STROKE);
            canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, mPaint);
            mPaint.setStyle(Style.FILL);

这里写图片描述

// 3. 画连接部分
                Path path = new Path();
                    // 跳到点1
                path.moveTo(mStickPoints[0].x, mStickPoints[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, mStickPoints[1].x, mStickPoints[1].y);
                path.close();
                canvas.drawPath(path, mPaint);
  • 画固定圆和拖拽圆:
            // 2. 画固定圆
                canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, mPaint); 
            // 1. 画拖拽圆
            canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, mPaint);
            // 恢复上次的保存状态
            canvas.restore();

完成上面的onDraw()函数后,将会得到一个静态效果,如下:
这里写图片描述
接下来要实现动态变化的效果

动态变化主要通过重写onTouchEvent函数,通过按下,移动,抬起三个事件来改变图形效果

1 。处理按下(MotionEvent.ACTION_DOWN)需要做的事:

根据按下的位置坐标,画出拖动点和由点1,2,3,4构成的封闭图形

   case MotionEvent.ACTION_DOWN:
                isOutofRange = false;
                isDisappear = false;//标记拖动圆为超出界限区域
                x = event.getRawX();//getRowX:触摸点相对于屏幕的坐标
                y = event.getRawY();
                updateDragCenter(x, y); //更新拖动圆(即执行onDraw函数,重绘拖动圆)

2。 处理移动(MotionEvent.ACTION_MOVE:)所需要做的事

移动过程中需要根据固定点和拖拽点的距离来改变固定点的半径,

需要判断固定点和移动点之间的距离是否大于farestDistance(即两点之间的最大限制距离,即控制界限区域的那个圆的半径)。

如果大于:断开两个圆之间的连线(此时通过getTempStickRadius()获取的固定圆的半径为0,所以固定圆将消失);
如果不大于:则动态改变拖拽圆的位置和大小,以及固定圆的大小。

case MotionEvent.ACTION_MOVE:
                x = event.getRawX();
                y = event.getRawY();
                updateDragCenter(x, y);

                // 处理断开事件
                float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter);
                if(distance > farestDistance){
                    isOutofRange = true;
                    invalidate();
                }
在这里需要注意的是:当用拖拽出界限区域后又拖拽回到界限区域内时未做任何事情,用户可以在这里加一个else执行执行自己的代码               

3 。处理手指抬起时需要做的事情

判断拖拽是否超出界限

超出界限:判断两个圆的圆心距离(在拖拽已经超出界限区域还要判断圆心距离,是因为在移动动作中,当用户移出界限区后又移回界限区时isOutofRange没有改变,但圆心距会小于farestDistance)

如果Distance>farestDistance:拖拽超出范围,断开, 松手, 消失

如果小于:拖拽超出范围,断开,放回去了,恢复

没有超出界限区: 松手,弹回去

c. 拖拽没超出范围, 松手,弹回去  

final PointF tempDragCenter = new PointF(mDragCenter.x, mDragCenter.y);
//值动画                   
ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f);
mAnim.addUpdateListener(new AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator mAnim) {
        // 0.0 -> 1.0f
        //获取动画执行的百分比(0.0 -> 1.0f)
       float percent = mAnim.getAnimatedFraction();
       //根据百分比获取两点之间的某个点坐标
       PointF p = GeometryUtil.getPointByPercent(tempDragCenter, mStickCenter, percent);
    updateDragCenter(p.x, p.y);
            }
        });
        mAnim.setInterpolator(new OvershootInterpolator(4));
                    mAnim.setDuration(500);
                    mAnim.start();
                }

辅助函数:

/**
     * 更新拖拽圆圆心坐标,并重绘界面
     * @param x
     * @param y
     */
    private void updateDragCenter(float x, float y) {
        mDragCenter.set(x, y);
        invalidate();//会调用onDraw()函数
    }
    //view的大小发生改变时被系统回调用
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        statusBarHeight = Utils.getStatusBarHeight(this);
    }

对整个自定义粘性控件的总结:

第一步:先自定义描绘控件所需要的参数常量
第二步:先通过onDraw绘画出一个想要的静态view效果图
第三步:通过重写onTouchEvent方法。分析清楚按下,移动,抬起时所需要改变的效果,在这三个动作发生时改变相应的参数并调用invalidate();方法,对view进行重绘

自定义控件暴露事件回调的方法:

1.声明一个接口:

interface GooViewListener{
     // 相应动作对应的回调方法(在view的onTouchEvent的不同情况下被调用)
     public void fun1();
       public void fun2();
       ..........
}

2.定义一个 GooViewListener接口变量:

public GooViewListener gvlistener;

3.为GooViewListener定义一个监听方法:

public void setGooViewListener(GooViewListener gvlistener){
         this.gvlistener=gvlistener;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值