自定义view-仿虎扑直播比赛界面的打赏按钮

作为一个资深篮球爱好者,我经常会用虎扑app看比赛直播,后来注意到文字直播界面右下角加了两个按钮,可以在直播过程中送虎扑币,为自己支持的球队加油,具体的效果如下图所示:

1701061

我个人觉得挺好玩的,所以决定自己实现下这个按钮,废话不多说,先看实现的效果吧:

hoopview

这个效果看起来和popupwindow差不多,但我是采用自定义view的方式来实现,下面说说过程。

首先从虎扑的效果可以看到,它这两个按钮时浮在整个界面之上的,所以它需要和FrameLayout结合使用,因此我让它的宽度跟随屏幕大小,高度根据dpi固定,它的实际尺寸时这样的:

1701062

另外这个view初始化出来我们看到可以分为三块,背景圆、圆内文字、圆上方数字,所以正常状态下,只需要在onDraw方法中画出这三块内容即可。先在初始化方法中将自定义的属性和画笔以及初始化数据准备好:

private void init(Context context, AttributeSet attrs) {
    //获取自定义属性
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.HoopView);
    mThemeColor = typedArray.getColor(R.styleable.HoopView_theme_color, Color.YELLOW);
    mText = typedArray.getString(R.styleable.HoopView_text);
    mCount = typedArray.getString(R.styleable.HoopView_count);

    mBgPaint = new Paint();
    mBgPaint.setAntiAlias(true);
    mBgPaint.setColor(mThemeColor);
    mBgPaint.setAlpha(190);
    mBgPaint.setStyle(Paint.Style.FILL);

    mPopPaint = new Paint();
    mPopPaint.setAntiAlias(true);
    mPopPaint.setColor(Color.LTGRAY);
    mPopPaint.setAlpha(190);
    mPopPaint.setStyle(Paint.Style.FILL_AND_STROKE);

    mTextPaint = new TextPaint();
    mTextPaint.setAntiAlias(true);
    mTextPaint.setColor(mTextColor);
    mTextPaint.setTextSize(context.getResources().getDimension(R.dimen.hoop_text_size));

    mCountTextPaint = new TextPaint();
    mCountTextPaint.setAntiAlias(true);
    mCountTextPaint.setColor(mThemeColor);
    mCountTextPaint.setTextSize(context.getResources().getDimension(R.dimen.hoop_count_text_size));

    typedArray.recycle();

    mBigRadius = context.getResources().getDimension(R.dimen.hoop_big_circle_radius);
    mSmallRadius = context.getResources().getDimension(R.dimen.hoop_small_circle_radius);
    margin = (int) context.getResources().getDimension(R.dimen.hoop_margin);
    mHeight = (int) context.getResources().getDimension(R.dimen.hoop_view_height);
    countMargin = (int) context.getResources().getDimension(R.dimen.hoop_count_margin);

    mDatas = new String[] {"1", "10", "100"};
    // 计算背景框改变的长度,默认是三个按钮
    mChangeWidth = (int) (2 * mSmallRadius * 3 + 4 * margin);

}

在onMeasure中测出view的宽度后,根据宽度计算出背景圆的圆心坐标和一些相关的数据值。

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    mWidth = getDefaultSize(widthSize, widthMeasureSpec);
    setMeasuredDimension(mWidth, mHeight);

    // 此时才测出了mWidth值,再计算圆心坐标及相关值
    cx = mWidth - mBigRadius;
    cy = mHeight - mBigRadius;
    // 大圆圆心
    circle = new PointF(cx, cy);
    // 三个按钮的圆心
    circleOne = new PointF(cx - mBigRadius - mSmallRadius - margin, cy);
    circleTwo = new PointF(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy);
    circleThree = new PointF(cx - mBigRadius - 5 * mSmallRadius - 3 * margin, cy);
    // 初始的背景框的边界即为大圆的四个边界点
    top = cy - mBigRadius;
    bottom = cy + mBigRadius;
}

因为这里面涉及到点击按钮展开和收缩的过程,所以我定义了如下几种状态,只有在特定的状态下才能进行某些操作。

private int mState = STATE_NORMAL;//当前展开收缩的状态
private boolean mIsRun = false;//是否正在展开或收缩

//正常状态
public static final int STATE_NORMAL = 0;
//按钮展开
public static final int STATE_EXPAND = 1;
//按钮收缩
public static final int STATE_SHRINK = 2;
//正在展开
public static final int STATE_EXPANDING = 3;
//正在收缩
public static final int STATE_SHRINKING = 4;

接下来就执行onDraw方法了,先看看代码:

@Override protected void onDraw(Canvas canvas) {
    switch (mState) {
        case STATE_NORMAL:
            drawCircle(canvas);
            break;
        case STATE_SHRINK:
        case STATE_SHRINKING:
            drawBackground(canvas);
            break;
        case STATE_EXPAND:
        case STATE_EXPANDING:
            drawBackground(canvas);
            break;
    }
    drawCircleText(canvas);
    drawCountText(canvas);
}

圆上方的数字和圆内的文字是整个过程中一直存在的,所以我将这两个操作放在switch之外,正常状态下绘制圆和之前两部分文字,点击展开时绘制背景框展开过程和文字,展开状态下再次点击绘制收缩过程和文字,当然在绘制背景框的方法中也需要不断绘制大圆,大圆也是一直存在的。

上面的绘制方法:

/**
 * 画背景大圆
 * @param canvas
 */
private void drawCircle(Canvas canvas) {
    left = cx - mBigRadius;
    right = cx + mBigRadius;
    canvas.drawCircle(cx, cy, mBigRadius, mBgPaint);
}


/**
 * 画大圆上面表示金币数的文字
 * @param canvas
 */
private void drawCountText(Canvas canvas) {
    canvas.translate(0, -countMargin);
    //计算文字的宽度
    float textWidth = mCountTextPaint.measureText(mCount, 0, mCount.length());
    canvas.drawText(mCount, 0, mCount.length(), (2 * mBigRadius - textWidth - 35) / 2, 0.2f, mCountTextPaint);
}


/**
 * 画大圆内的文字
 * @param canvas
 */
private void drawCircleText(Canvas canvas) {
    StaticLayout layout = new StaticLayout(mText, mTextPaint, (int) (mBigRadius * Math.sqrt(2)), Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, true);
    canvas.translate(mWidth - mBigRadius * 1.707f, mHeight - mBigRadius * 1.707f);
    layout.draw(canvas);
    canvas.save();
}


/**
 * 画背景框展开和收缩
 * @param canvas
 */
private void drawBackground(Canvas canvas) {
    left = cx - mBigRadius - mChange;
    right = cx + mBigRadius;
    canvas.drawRoundRect(left, top, right, bottom, mBigRadius, mBigRadius, mPopPaint);
    if ((mChange > 0) && (mChange <= 2 * mSmallRadius + margin)) {
        // 绘制第一个按钮
        canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
        // 绘制第一个按钮内的文字
        canvas.drawText(mDatas[0], cx - (mBigRadius - mSmallRadius) - mChange, cy + 15, mTextPaint);
    } else if ((mChange > 2 * mSmallRadius + margin) && (mChange <= 4 * mSmallRadius + 2 * margin)) {
        // 绘制第一个按钮
        canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint);
        // 绘制第一个按钮内的文字
        canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 20, cy + 15, mTextPaint);

        // 绘制第二个按钮
        canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
        // 绘制第二个按钮内的文字
        canvas.drawText(mDatas[1], cx - mChange - 20, cy + 15, mTextPaint);
    } else if ((mChange > 4 * mSmallRadius + 2 * margin) && (mChange <= 6 * mSmallRadius + 3 * margin)) {
        // 绘制第一个按钮
        canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint);
        // 绘制第一个按钮内的文字
        canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint);

        // 绘制第二个按钮
        canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint);
        // 绘制第二个按钮内的文字
        canvas.drawText(mDatas[1], cx - mBigRadius - 3 * mSmallRadius - 2 * margin - 25, cy + 15, mTextPaint);

        // 绘制第三个按钮
        canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
        // 绘制第三个按钮内的文字
        canvas.drawText(mDatas[2], cx - mChange - 34, cy + 15, mTextPaint);
    } else  if (mChange > 6 * mSmallRadius + 3 * margin) {
        // 绘制第一个按钮
        canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint);
        // 绘制第一个按钮内的文字
        canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint);

        // 绘制第二个按钮
        canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint);
        // 绘制第二个按钮内的文字
        canvas.drawText(mDatas[1], cx - mBigRadius - 3 * mSmallRadius - 2 * margin - 25, cy + 15, mTextPaint);

        // 绘制第三个按钮
        canvas.drawCircle(cx - mBigRadius - 5 * mSmallRadius - 3 * margin, cy, mSmallRadius, mBgPaint);
        // 绘制第三个按钮内的文字
        canvas.drawText(mDatas[2], cx - mBigRadius - 5 * mSmallRadius - 3 * margin - 34, cy + 15, mTextPaint);
    }
    drawCircle(canvas);

}

然后是点击事件的处理,只有触摸点在大圆内时才会触发展开或收缩的操作,点击小圆时提供了一个接口给外部调用。

@Override public boolean onTouchEvent(MotionEvent event) {
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            //如果点击的时候动画在进行,不处理
            if (mIsRun) return true;
            PointF pointF = new PointF(event.getX(), event.getY());
            if (isPointInCircle(pointF, circle, mBigRadius)) { //如果触摸点在大圆内,根据弹出方向弹出或者收缩按钮
                if ((mState == STATE_SHRINK || mState == STATE_NORMAL) && !mIsRun) {
                    //展开
                    mIsRun = true;//这是必须先设置true,因为onAnimationStart在onAnimationUpdate之后才调用
                    showPopMenu();
                } else {
                    //收缩
                    mIsRun = true;
                    hidePopMenu();
                }
            } else { //触摸点不在大圆内
                if (mState == STATE_EXPAND) { //如果是展开状态
                    if (isPointInCircle(pointF, circleOne, mSmallRadius)) {
                        listener.clickButton(this, Integer.parseInt(mDatas[0]));
                    } else if (isPointInCircle(pointF, circleTwo, mSmallRadius)) {
                        listener.clickButton(this, Integer.parseInt(mDatas[1]));
                    } else if (isPointInCircle(pointF, circleThree, mSmallRadius)) {
                        listener.clickButton(this, Integer.parseInt(mDatas[2]));
                    }
                    mIsRun = true;
                    hidePopMenu();
                }
            }
            break;
    }
    return super.onTouchEvent(event);
}

展开和收缩的动画是改变背景框的宽度属性的动画,并监听这个属性动画,在宽度值改变的过程中去重新绘制整个view。因为一开始我就确定了大圆小圆的半径和小圆与背景框之间的间距,所以初始化时已经计算好了背景框的宽度:

mChangeWidth = (int) (2 * mSmallRadius * 3 + 4 * margin);
/**
 * 弹出背景框
 */
private void showPopMenu() {
    if (mState == STATE_SHRINK || mState == STATE_NORMAL) {
        ValueAnimator animator = ValueAnimator.ofInt(0, mChangeWidth);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override public void onAnimationUpdate(ValueAnimator animation) {
                if (mIsRun) {
                    mChange = (int) animation.getAnimatedValue();
                    invalidate();
                } else {
                    animation.cancel();
                    mState = STATE_NORMAL;
                }
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                mIsRun = true;
                mState = STATE_EXPANDING;
            }


            @Override public void onAnimationCancel(Animator animation) {
                super.onAnimationCancel(animation);
                mIsRun = false;
                mState = STATE_NORMAL;
            }


            @Override public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                mIsRun = false;
                //动画结束后设置状态为展开
                mState = STATE_EXPAND;
            }
        });
        animator.setDuration(500);
        animator.start();
    }
}
/**
 * 隐藏弹出框
 */
private void hidePopMenu() {
    if (mState == STATE_EXPAND) {
        ValueAnimator animator = ValueAnimator.ofInt(mChangeWidth, 0);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override public void onAnimationUpdate(ValueAnimator animation) {
                if (mIsRun) {
                    mChange = (int) animation.getAnimatedValue();
                    invalidate();
                } else {
                    animation.cancel();
                }
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                mIsRun = true;
                mState = STATE_SHRINKING;
            }


            @Override public void onAnimationCancel(Animator animation) {
                super.onAnimationCancel(animation);
                mIsRun = false;
                mState = STATE_EXPAND;
            }


            @Override public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                mIsRun = false;
                //动画结束后设置状态为收缩
                mState = STATE_SHRINK;
            }
        });
        animator.setDuration(500);
        animator.start();
    }
}

这个过程看起来是弹出或收缩,实际上宽度值每改变一点,就将所有的组件重绘一次,只是文字和大圆等内容的尺寸及位置都没有变化,只有背景框的宽度值在变,所以才有这种效果。

在xml中的使用:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_marginBottom="20dp"
    android:layout_alignParentRight="true"
    android:orientation="vertical">

    <com.xx.hoopcustomview.HoopView
        android:id="@+id/hoopview1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginRight="10dp"
        app:text="支持火箭"
        app:count="1358"
        app:theme_color="#31A129"/>

    <com.xx.hoopcustomview.HoopView
        android:id="@+id/hoopview2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginRight="10dp"
        app:text="热火无敌"
        app:count="251"
        app:theme_color="#F49C11"/>
</LinearLayout>

activity中使用:

hoopview1 = (HoopView) findViewById(R.id.hoopview1);
hoopview1.setOnClickButtonListener(new HoopView.OnClickButtonListener() {
    @Override public void clickButton(View view, int num) {
        Toast.makeText(MainActivity.this, "hoopview1增加了" + num, Toast.LENGTH_SHORT).show();
    }
});

大致实现过程就是这样,与原始效果还是有点区别,我这个还有很多瑕疵,比如文字的位置居中问题,弹出或收缩时,小圆内的文字的旋转动画我没有实现。

代码地址:
https://github.com/shenhuniurou/HoopCustomView

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容

  • 前言 NBA全明星周末!重要事情说一遍!作为一个经常在虎扑上看NBA直播的小球迷,老早就留意到了打赏加油的小控件,...
    dengzq阅读 993评论 0 2
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,914评论 25 707
  • 主题一:关于婚姻的几个核心主题--1、经济。双方收入高低、金钱的支配权(各花各的、一方主导、共同账户及商议)、...
    汤蔚阅读 425评论 0 3
  • 以前看到过一句话,一直都在脑中,难以忘怀,越是长大,越是能深刻感觉:“越是深夜,越是孤独”。我想大概越是夜深人静就...
    朝露2阅读 202评论 1 1
  • 网络时代,越来越多的人适应、甚至喜欢上了“快餐式”消费。随之而来的影响,我们没有了“英雄”,满文具店的是一次性水笔...
    浩哥随思录阅读 303评论 0 0