仿QQ微信的带消息数量提醒框


//这年头没效果都不好意思说话,是不是小习子



//首先是自定义的类AdhesionLayout    //注释都在

//这个类是实现画圆和拉伸的判断!!!!直接复制,完美运行,带你装逼,带你飞

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.BounceInterpolator;
import android.widget.RelativeLayout;


/**
 * 粘合布局
 * @author linzewu
 * @date 2016/5/23
 */
public class AdhesionLayout extends RelativeLayout{

    /**
     * 头部圆
     */
    private Circle mHeaderCircle = new Circle();

    /**
     * 尾部圆
     */
    private Circle mFooterCircle = new Circle();

    /**
     * 画笔
     */
    private Paint mPaint = new Paint();

    /**
     * 画贝塞尔曲线的Path对象
     */
    private Path mPath = new Path();

    /**
     * 默认粘连的最大长度
     */
    private float mMaxAdherentLength = 150;

    /**
     * 头部圆缩小时不能小于这个最小半径
     */
    private float mMinHeaderCircleRadius = 4;

    /**
     * 用户添加的视图(可以不添加)
     */
    private View mView;

    /**
     * 监听粘连是否断掉的监听器
     */
    private OnAdherentListener mOnAdherentListener;

    /**
     * 是否粘连着
     */
    private boolean isAdherent = true;

    /**
     * 粘连的颜色
     */
    private int mColor;

    /**
     * 头部圆的当前半径
     */
    private float mCurrentRadius;

    /**
     * 是否第一次onSizeChanged
     */
    private boolean isFirst = true;

    /**
     * 是否正在进行动画中
     */
    private boolean isAnim = false;

    /**
     * 是否允许可以扯断
     */
    private boolean isSnap = true;

    /**
     * 本View宽度、高度
     */
    private int mWidth;
    private int mHeight;

    /**
     * 本View的左上角x、y
     */
    private float mX;
    private float mY;

    /**
     * 记录按下的x、y
     */
    float mDownX;
    float mDownY;

    /**
     * 按下的时候,记录此刻尾部圆的圆心x、y
     */
    float mDownFooterCircleX;
    float mDownFooterCircleY;


    /**
     * 构造函数
     *
     * @param context
     */
    public AdhesionLayout(Context context) {
        super(context);
        init();
    }

    /**
     * 构造函数
     *
     * @param context
     * @param attrs
     */
    public AdhesionLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 构造函数
     *
     * @param context
     * @param attrs
     * @param defStyle
     */
    public AdhesionLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    /**
     * 初始化
     */
    private void init() {

        // 透明背景
        setBackgroundColor(Color.TRANSPARENT);

        // 设置颜色
        mColor = Color.rgb(247,82,49);

        // 设置画笔
        mPaint.setColor(mColor);
        mPaint.setAntiAlias(true);
    }


    /**
     * 当视图组大小发生变化   只调用一次
     * 关于onSizeChanged()的调用问题,原则是只有视图大小改变的时候调用,但是由于是首次初始化
     * 因此,在视图初始化的时候,会调用一次,即mWidth mHeight只会赋值一次
     * @param w
     * @param h
     * @param oldw
     * @param oldh
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (isFirst && w > 0 && h > 0) {
            isFirst = false;
            mView = getChildAt(0);
            mWidth = w;
            mHeight = h;
            mX = getX();
            mY = getY();
            reset();
        }
    }

    /**
     * 重置所有参数
     */
    public void reset() {
        //还原视图宽高为默认值
        setWidthAndHeight(mWidth, mHeight);  //设置宽高
        mCurrentRadius = mHeaderCircle.radius = mFooterCircle.radius = (float)(Math.min(mWidth,mHeight) / 2) - 2;
        mHeaderCircle.x = mFooterCircle.x = mWidth / 2;  //使得头部圆置于视图组的中点
        mHeaderCircle.y = mFooterCircle.y = mHeight / 2; //使得尾部圆置于视图的的中心
        if (mView != null) {                 //让子视图置于左上角
            mView.setX(0);
            mView.setY(0);
        }
        isAnim = false;

    }

    /**
     * 绘制方法
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.TRANSPARENT);
        //绘制头部圆
        canvas.drawCircle(mHeaderCircle.x, mHeaderCircle.y, mCurrentRadius, mPaint);
        //绘制尾部圆
        canvas.drawCircle(mFooterCircle.x, mFooterCircle.y, mFooterCircle.radius, mPaint);
        if (isAdherent)
            drawBezier(canvas);
    }

    /**
     * 画贝塞尔曲线
     *
     * 知识点补充:
     * 1、atan : 反正切值    因为tan(atanA)=A,互为反函数 所以 atanA = 角度值
     *    double atan(double d)  返回对应角度的反正切值
     * 2、sin : 正弦值
     *    double sin(double d)  返回对应角度的正弦值
     * 3、cos : 余弦值
     *    double cos(double d)  返回对应角度的余弦值
     * @param canvas
     */
    private void drawBezier(Canvas canvas) {
        
        /* 求三角函数 */
        //atan为角度值
        float atan = (float) Math.atan((mFooterCircle.y - mHeaderCircle.y) / (mFooterCircle.x - mHeaderCircle.x));
        //获取sin、cos值
        float sin = (float) Math.sin(atan);
        float cos = (float) Math.cos(atan);
        
        /* 四个点 */
        float headerX1 = mHeaderCircle.x - mCurrentRadius * sin;
        float headerY1 = mHeaderCircle.y + mCurrentRadius * cos;

        float headerX2 = mHeaderCircle.x + mCurrentRadius * sin;
        float headerY2 = mHeaderCircle.y - mCurrentRadius * cos;

        float footerX1 = mFooterCircle.x - mFooterCircle.radius * sin;
        float footerY1 = mFooterCircle.y + mFooterCircle.radius * cos;

        float footerX2 = mFooterCircle.x + mFooterCircle.radius * sin;
        float footerY2 = mFooterCircle.y - mFooterCircle.radius * cos;

        /**
         * 操作点
         */
        float anchorX = ( mHeaderCircle.x + mFooterCircle.x ) / 2;
        float anchorY = ( mHeaderCircle.y + mFooterCircle.y ) / 2;
    
        /* 画贝塞尔曲线 */
        mPath.reset();
        mPath.moveTo(headerX1, headerY1);
        mPath.quadTo(anchorX, anchorY, footerX1, footerY1);
        mPath.lineTo(footerX2, footerY2);
        mPath.quadTo(anchorX, anchorY, headerX2, headerY2);
        mPath.lineTo(headerX1, headerY1);
        canvas.drawPath(mPath, mPaint);
    }

    /**
     * 触摸方法
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        /**
         * 如果正在执行动画,不接受任何触摸事件
         */
        if (isAnim)
            return true;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                /**
                 * 重新设置布局大小: 填充父布局
                 * 注意:只有在点击以及移动的时候,才会让视图暂时变为填充满父布局,
                 *      普通情况下,视图大小为默认设置的宽高,不会填充父布局,这样避免了其他控件的点击事件被屏蔽
                 */
                setWidthAndHeight(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
                /**
                 * 把两个圆的坐标由原来的坐标转化为填充父布局之后的坐标
                 */
                mHeaderCircle.x = mX + mHeaderCircle.x;
                mHeaderCircle.y = mY + mHeaderCircle.y;
                mFooterCircle.x = mX + mFooterCircle.x;
                mFooterCircle.y = mY + mFooterCircle.y;
                mDownX = mX + event.getX();
                mDownY = mY + event.getY();
                mDownFooterCircleX = mFooterCircle.x;
                mDownFooterCircleY = mFooterCircle.y;
                if (mView != null) {
                    mView.setX(mFooterCircle.x - mWidth / 2);
                    mView.setY(mFooterCircle.y - mHeight / 2);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mFooterCircle.x = mDownFooterCircleX + (event.getX() - mDownX);
                mFooterCircle.y = mDownFooterCircleY + (event.getY() - mDownY);
                if (mView != null) {
                    mView.setX(mFooterCircle.x - mWidth / 2);
                    mView.setY(mFooterCircle.y - mHeight / 2);
                }
                doAdhere();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                isAnim = true;
                if (isAdherent)
                    startAnim();
                else if (mOnAdherentListener != null) {
                    mFooterCircle.radius = 0;
                    mOnAdherentListener.onDismiss();
                }
                break;
        }
        invalidate();
        return true;
    }

    /**
     * 处理粘连效果逻辑
     *
     * 知识补充:
     *        Math.sqrt(x)   返回x的平方根
     *        Math.pow(x,y)  返回x的y次方
     */
    private void doAdhere() {
        float distance = (float) Math.sqrt(Math.pow(mFooterCircle.x - mHeaderCircle.x, 2) + Math.pow(mFooterCircle.y - mHeaderCircle.y, 2));
        float scale = 1 - distance / mMaxAdherentLength;
        mCurrentRadius = Math.max(mHeaderCircle.radius * scale, mMinHeaderCircleRadius);

        /**
         * 如果拉扯距离大于设定的最大距离并且此时设置了可以扯断
         */
        if (distance > mMaxAdherentLength && isSnap) {
            isAdherent = false;
            mCurrentRadius = 0;
        }
        else
            isAdherent = true;
    }

    /**
     * 开始粘连动画
     */
    private void startAnim() {
        
        /* x方向 */
        ValueAnimator xValueAnimator = ValueAnimator.ofFloat(mFooterCircle.x, mX + mWidth / 2);
        xValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mFooterCircle.x = (float) animation.getAnimatedValue();
                //动画执行的同时不断调用,不断刷新视图
                invalidate();
            }
        });

        /* y方向 */
        ValueAnimator yValueAnimator = ValueAnimator.ofFloat(mFooterCircle.y, mY + mHeight / 2);
        yValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //动画执行的同时不断调用,不断刷新视图
                mFooterCircle.y = (float) animation.getAnimatedValue();
                invalidate();
            }
        });

        /* 用户添加的视图x、y方向 */
        ObjectAnimator objectAnimator = null;
        if (mView != null) {
            PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("X", mFooterCircle.x - mWidth / 2, mX);
            PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("Y", mFooterCircle.y - mHeight / 2, mY);
            objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mView, pvhX, pvhY);
        }
         
        /* 动画集合 */
        AnimatorSet animSet = new AnimatorSet();
        if (mView != null)
            animSet.play(xValueAnimator).with(yValueAnimator).with(objectAnimator);
        else
            animSet.play(xValueAnimator).with(yValueAnimator);
        animSet.setInterpolator(new BounceInterpolator());
        animSet.setDuration(400);
        animSet.start();
        animSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                //动画结束的时候,重置所有参数
                reset();
            }
        });

    }

    /**
     * 设置宽和高
     *
     * @param width
     * @param height
     */
    private void setWidthAndHeight(int width, int height) {
        ViewGroup.LayoutParams layoutParams = getLayoutParams();
        layoutParams.width = width;
        layoutParams.height = height;
        setLayoutParams(layoutParams);

    }

    /**
     * 对外接口:设置颜色
     *
     * @param color
     */
    public void setColor(int color) {
        mColor = color;
        mPaint.setColor(mColor);
    }

    /**
     * 对外接口:设置是否可以扯断
     */
    public void setSnapEnable(boolean isSnap) {
        this.isSnap = isSnap;
    }

    /**
     * 对外接口:设置粘连的最大长度
     *
     * @param maxAdherentLength
     */
    public void setMaxAdherentLength(int maxAdherentLength) {
        mMaxAdherentLength = maxAdherentLength;
    }

    /**
     * 对外接口:设置头部的最小半径
     *
     * @param minHeaderCircleRadius
     */
    public void setMinHeaderCircleRadius(int minHeaderCircleRadius) {
        mMinHeaderCircleRadius = minHeaderCircleRadius;
    }

    /**
     * 对外接口:设置监听器
     *
     * @param onAdherentListener
     */
    public void setOnAdherentListener(OnAdherentListener onAdherentListener) {
        mOnAdherentListener = onAdherentListener;
    }

    /**
     * 圆类
     */
    private class Circle {
        public float x;
        public float y;
        public float radius;
    }

    /**
     * 监听器
     */
    public interface OnAdherentListener {
        public void onDismiss();
    }
}


//Main实现的内容

public class Main3Activity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main3);
        //初始化控件
        AdhesionLayout adhesionLayout = (AdhesionLayout) findViewById(R.id.chen);
        adhesionLayout.setOnAdherentListener(new AdhesionLayout.OnAdherentListener() {
            @Override
            public void onDismiss() {

            }
        });

    }
}



//main的布局

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.chenqq.Main3Activity">

   <com.example.chenxuqq.AdhesionLayout
       android:id="@+id/chen"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content">
       <TextView
           android:layout_width="50dp"
           android:layout_height="50dp"
           android:gravity="center"
           android:textColor="@android:color/white"
           android:textSize="22dp"
           android:text="8"/>
   </com.example.chenxuqq.AdhesionLayout>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="旭哥好!"/>

</RelativeLayout>





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值