您的位置:首页 > 其它

仿QQ拖动删除未读消息个数气泡之二

2015-09-16 10:52 162 查看
仿QQ拖动删除未读消息个数气泡这篇文章中,模仿了QQ的删除未读消息气泡,不过也遗留了一个问题,当时为了让气泡能够在全屏范围内拖动,不能将其放在布局文件xml中,而是采用了在主布局加载完成后用addView方法动态加载气泡,这种方式不太好,因为它需要自己计算在全屏范围中,气泡放在什么地方,这需要参造物,而且如果把气泡放在listview中的话,也不可能给每个Item都去动态计算增加气泡,所以这种方式不够完善。

经过尝试,改用另一种方式来处理,可以将气泡放置在布局xml中,也不需要用addView来动态添加了。这篇博客就来讲解气泡的全屏拖动,而随手指移动用贝塞尔曲线画连接请参考上篇博客,这里不再重复讲解。来看效果



先说下原理。为了实现气泡的全屏拖动,我们需要两个自定义控件,一个RoundNumber,它就是放置在XML中的气泡控件,不过它并不随着手势移动,因为它的大小是有限的,移动了也看不到,而另外一个BounceCircle,它的大小是全屏的,随手势移动的是它。RoundNumber可以有多个,但BounceCircle只有一个。初始状态时,BounceCircle是隐藏的,RoundNumber是显示的,当我们按下RoundNumber时,立即将BounceCircle显示,而将RoundNumber隐藏,要注意,这时手指移动依然会是在RoundNumber的onTouchEvent事件中,而不是BounceCircle的,为了让BounceCircle随手势移动,我们要在RoundNumber的onTouchEvent中,调用回调方法,而最终调用到BounceCircle的onDraw函数,来实现随手势移动。

下面看具体实现,首先是RoundNumber

public class RoundNumber extends View {
    private int radius; // 圆形半径
    private float circleX; // 圆心x坐标
    private float circleY; // 圆心y坐标

    private Paint circlePaint; // 圆形画笔
    private TextPaint textPaint; // 文字画笔
    private int textSize; // 字体大小,单位SP
    private Paint.FontMetrics textFontMetrics; // 字体
    private float textMove; // 为了让文字居中,需要移动的距离

    private String message = "1";
    private boolean firstInit = true;
    private Context mContext;

    private ClickListener mClickListener;

    public RoundNumber(Context context, AttributeSet attrs) {
        super(context, attrs);

        mContext = context;

        initPaint();
    }

    /**
     * 初始化
     */
    private void initPaint() {
        circlePaint = new Paint();
        circlePaint.setColor(Color.RED);
        circlePaint.setAntiAlias(true);

        textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(Color.WHITE);
    }

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

        if (firstInit) {
            firstInit = false;

            radius = w / 2;
            int[] position = new int[2];
            getLocationOnScreen(position);

            circleX = radius;
            circleY = radius;

            textSize = radius; // 根据圆半径来设置字体大小
            textPaint.setTextAlign(Paint.Align.CENTER);
            textPaint.setTextSize(Util.sp2px(mContext, textSize));
            textFontMetrics = textPaint.getFontMetrics();
            textMove = -textFontMetrics.ascent - (-textFontMetrics.ascent + textFontMetrics.descent) / 2; // drawText从baseline开始,baseline的值为0,baseline的上面为负值,baseline的下面为正值,即这里ascent为负值,descent为正值,比如ascent为-20,descent为5,那需要移动的距离就是20 - (20 + 5)/ 2
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawCircle(circleX, circleY, radius, circlePaint);
        canvas.drawText(message, circleX, circleY + textMove, textPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (MainActivity.isTouchable) {
                    if (mClickListener != null) {
                        MainActivity.isTouchable = false;
                        getParent().requestDisallowInterceptTouchEvent(true); // 不允许父控件处理TouchEvent,当父控件为ListView这种本身可滑动的控件时必须要控制
                        mClickListener.onDown();
                    }
                    return true;
                }

                return false;
            case MotionEvent.ACTION_MOVE:
                if (mClickListener != null) { // 注意这里要用getRaw来获取手指当前所处的相对整个屏幕的坐标
                    mClickListener.onMove(event.getRawX(), event.getRawY() - Util.getTopBarHeight((Activity)  mContext));
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mClickListener != null) {
                    getParent().requestDisallowInterceptTouchEvent(false); // 将控制权还给父控件
                    mClickListener.onUp();
                }
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 设置显示内容
     * @param message
     */
    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * 获取显示内容
     * @return
     */
    public String getMessage() {
        return message;
    }

    public interface ClickListener {
        void onDown(); // 手指按下

        void onMove(float curX, float curY); // 手指移动

        void onUp(); // 手指抬起
    }

    public void setClickListener(ClickListener listener) {
        mClickListener = listener;
    }
}


可以看到,RoundNumber 这个自定义控件,它本身的onDraw方法中,只画了一个红色圆,和圆中的白色数字,而在它的onTouchEvent中,我们都是使用的都是类似mClickListener.***这样的方法,而这个mClickListener是我们自己定义的一个接口,它用来回调,在回调中去真正处理随手势移动的逻辑。另外,这里还需要注意的是,在手指按下时,我们调用了getParent().requestDisallowInterceptTouchEvent(true);方法,这是用来让父控件放弃控制权,因为在一些本身可以随手指移动的控件,例如ListView,不加这个处理,会导致事件被父类消费了,所以这里要控制一下,同样道理,手指抬起时,要调用getParent().requestDisallowInterceptTouchEvent(false);将控制权还给父类。

另外这里还用到了一个全局变量MainActivity.isTouchable,这个值初始情况为true,在有RoundNumber被按下时,就会赋值为false,完后等手指抬起,处理完动画后,才会重新变成true,设置这样一个变量是为了防止同时有多个RoundNumber被处理,因为只有一个BounceCircle。完后在MainActivity中去设置回调

unreadMessage = (RoundNumber) findViewById(R.id.unread_message);
unreadMessage.setMessage("3");
unreadMessage.setClickListener(new RoundNumber.ClickListener() {
@Override
public void onDown() {
int[] position = new int[2];
unreadMessage.getLocationOnScreen(position);

int radius = unreadMessage.getWidth() / 2;
circle.down(radius, position[0] + radius, position[1] - Util.getTopBarHeight(MainActivity.this) + radius, unreadMessage.getMessage());
circle.setVisibility(View.VISIBLE); // 显示全屏范围的BounceCircle

unreadMessage.setVisibility(View.INVISIBLE); // 隐藏固定范围的RoundNumber
circle.setOrginView(unreadMessage);
}

@Override
public void onMove(float curX, float curY) {
circle.move(curX, curY);
}

@Override
public void onUp() {
circle.up();
}
});


这里的unreadMessage是RoundNumber,而circle是BounceCircle,可以看到,最终的处理还是在BounceCircle中

BounceCircle的代码如下:

public class BounceCircle extends View {
private Context mContext;

private Paint circlePaint; // 圆形/连线画笔
private TextPaint textPaint; // 文字画笔
private Paint.FontMetrics textFontMetrics; // 字体
private Path path;

private int radius; // 移动圆形半径
private float textMove; // 为了让文字居中,需要移动的距离

private float curX; // 当前x坐标
private float curY; // 当前y坐标
private float circleX; // 固定圆的圆心x坐标
private float circleY; // 固定圆的圆心y坐标
private float ratio = 1; // 圆缩放的比例,随着手指的移动,固定的圆越来越小
private float ratioLimit = 0.2f; // 固定圆最小的缩放比例,小于该比例时就直接消失
private int distanceLimit = 100; // 固定圆和移动圆的圆心之间距离的限值,单位DP(配合ratioLimit使用)
private int textSize; // 字体大小,单位SP

private int animationTime = 200; // 抖动动画执行的时间
private int animationTimes = 1; //  抖动动画执行次数
private boolean needDraw = true; // 是否需要执行onDraw方法

private FinishListener mFinishListener; // 自定义接口,用来回调
private String message = "1"; // 显示的数字的初始值

private Bitmap[] explosionAnim; // 爆炸动画
private boolean animStart; // 动画开始
private int animNumber = 5; // 动画帧的个数
private int curAnimNumber; // 动画播放的当前帧
private int animInterval = 200; // 动画帧之间的间隔
private int animWidth; // 动画帧的宽度
private int animHeight; // 动画帧的高度

private View originalView;

public BounceCircle(Context context, AttributeSet attrs) {
super(context, attrs);

mContext = context;

initPaint();
}

/**
* 初始化Paint
*/
private void initPaint() {
circlePaint = new Paint();
circlePaint.setColor(Color.RED);
circlePaint.setAntiAlias(true);

distanceLimit = Util.dip2px(mContext, distanceLimit);

textPaint = new TextPaint();
textPaint.setAntiAlias(true);
textPaint.setColor(Color.WHITE);

path = new Path();
}

/**
* 初始化爆炸动画
*/
private void initAnim() {
if (explosionAnim == null) {
explosionAnim = new Bitmap[animNumber];
explosionAnim[0] = BitmapFactory.decodeResource(getResources(), R.mipmap.explosion_one);
explosionAnim[1] = BitmapFactory.decodeResource(getResources(), R.mipmap.explosion_two);
explosionAnim[2] = BitmapFactory.decodeResource(getResources(), R.mipmap.explosion_three);
explosionAnim[3] = BitmapFactory.decodeResource(getResources(), R.mipmap.explosion_four);
explosionAnim[4] = BitmapFactory.decodeResource(getResources(), R.mipmap.explosion_five);

// 动画每帧的长宽都是一样的,取一个即可
animWidth = explosionAnim[0].getWidth();
animHeight = explosionAnim[0].getHeight();
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

if (needDraw) {
// 画固定圆
if (ratio >= ratioLimit) {
canvas.drawCircle(circleX, circleY, radius * ratio, circlePaint);
}

// 画移动圆和连线
if (curX != 0 && curY != 0) {
canvas.drawCircle(curX, curY, radius, circlePaint);
if (ratio >= ratioLimit) {
drawLinePath(canvas);
}
}

// 数字要最后画,否则会被连线遮掩
if (curX != 0 && curY != 0) { // 移动圆里面的数字
canvas.drawText(message, curX, curY + textMove, textPaint);
} else { // 只有初始时需要绘制固定圆里面的数字
canvas.drawText(message, circleX, circleY + textMove, textPaint);
}
}

if (animStart) { // 动画进行中
if (curAnimNumber < animNumber) {
canvas.drawBitmap(explosionAnim[curAnimNumber], curX - animWidth / 2, curY - animHeight / 2, null);
curAnimNumber++;
if (curAnimNumber == 1) { // 第一帧立即执行
invalidate();
} else { // 其余帧每隔固定时间执行
postInvalidateDelayed(animInterval);
}
} else { // 动画结束
animStart = false;
curAnimNumber = 0;
recycleBitmap();
setVisibility(View.INVISIBLE); // 隐藏BounceCircle
curX = 0;
curY = 0;

MainActivity.isTouchable = true;

// 删除后的回调
if (mFinishListener != null) {
mFinishListener.onFinish();
}
}
}
}

/**
* 回收Bitmap资源
*/
private void recycleBitmap() {
if (explosionAnim != null && explosionAnim.length != 0) {
for (int i = 0; i < explosionAnim.length; i++) {
if (explosionAnim[i] != null && !explosionAnim[i].isRecycled()) {
explosionAnim[i].recycle();
explosionAnim[i] = null;
}
}

explosionAnim = null;
}
}

/**
* 画固定圆和移动圆之间的连线
* @param canvas
*/
private void drawLinePath(Canvas canvas) {
path.reset();

float distance = (float) Util.distance(circleX, circleY, curX, curY); // 移动圆和固定圆圆心之间的距离
float sina = (curY - circleY) / distance; // 移动圆圆心和固定圆圆心之间的连线与X轴相交形成的角度的sin值
float cosa = (circleX - curX) / distance; // 移动圆圆心和固定圆圆心之间的连线与X轴相交形成的角度的cos值

path.moveTo(circleX - sina * radius * ratio, circleY - cosa * radius * ratio); // A点坐标
path.lineTo(circleX + sina * radius * ratio, circleY + cosa * radius * ratio); // AB连线
path.quadTo((circleX + curX) / 2, (circleY + curY) / 2, curX + sina * radius, curY + cosa * radius); // 控制点为两个圆心的中间点,二阶贝塞尔曲线,BC连线
path.lineTo(curX - sina * radius, curY - cosa * radius); // CD连线
path.quadTo((circleX + curX) / 2, (circleY + curY) / 2, circleX - sina * radius * ratio, circleY - cosa * radius * ratio); // 控制点也是两个圆心的中间点,二阶贝塞尔曲线,DA连线

canvas.drawPath(path, circlePaint);
}

/**
* 计算固定圆缩放的比例
* @param distance
* @return
*/
private void calculateRatio(float distance) {
ratio = (distanceLimit - distance) / distanceLimit;
}

/**
* 抖动动画
* @param counts
*/
public void shakeAnimation(int counts) {
// 避免动画抖动的频率过大,所以除以2,另外,抖动的方向跟手指滑动的方向要相反
Animation translateAnimation = new TranslateAnimation((circleX - curX) / 2, 0, (circleY - curY) / 2, 0);
translateAnimation.setInterpolator(new CycleInterpolator(counts));
translateAnimation.setDuration(animationTime);
startAnimation(translateAnimation);

translateAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}

@Override
public void onAnimationEnd(Animation animation) { // 抖动动画结束时,显示以前的RoundNumber,隐藏BounceCircle
if (originalView != null) {
originalView.setVisibility(View.VISIBLE);
}

setVisibility(View.INVISIBLE);

MainActivity.isTouchable = true;
}

@Override
public void onAnimationRepeat(Animation animation) {
}
});
}

public interface FinishListener {
void onFinish();
}

public void setFinishListener(FinishListener finishListener) {
mFinishListener = finishListener;
}

public void move(float curX, float curY) {
this.curX = curX;
this.curY = curY;
calculateRatio((float) Util.distance(curX, curY, circleX, circleY));

invalidate();
}

public void up() {
if (ratio > ratioLimit) { // 没有超出最大移动距离,手抬起时需要让移动圆回到固定圆的位置
shakeAnimation(animationTimes);

curX = 0;
curY = 0;
ratio = 1;

} else { // 超出最大移动距离
needDraw = false;
animStart = true;

initAnim();
}

invalidate();
}

public void down(int radius, float circleX, float circleY, String message) {
needDraw = true; // 由于BounceCircle是公用的,每次进来时都要确保needDraw的值为true

this.radius = radius;
this.circleX = circleX;
this.circleY = circleY;
this.message = message;

textSize = radius;
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(Util.sp2px(mContext, textSize));
textFontMetrics = textPaint.getFontMetrics();
textMove = -textFontMetrics.ascent - (-textFontMetrics.ascent + textFontMetrics.descent) / 2; // drawText从baseline开始,baseline的值为0,baseline的上面为负值,baseline的下面为正值,即这里ascent为负值,descent为正值,比如ascent为-20,descent为5,那需要移动的距离就是20 - (20 + 5)/ 2

invalidate();
}

/**
* 设置按下时被隐藏的View
* @param view
*/
public void setOrginView(View view) {
originalView = view;
}
}




这个跟上一篇博客中讲的基本是一样的,唯一的区别是这里没有onTouchEvent的处理,因为手势的按键响应在RoundNumber的onTouchEvent中,完后将事件传递到Bouncecircle,再通过down,move和up三个方法来处理手势的最终操作

源码下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: