作為一個資深籃球愛好者,我經(jīng)常會用虎撲app看比賽直播招刨,后來注意到文字直播界面右下角加了兩個按鈕霎俩,可以在直播過程中送虎撲幣,為自己支持的球隊加油沉眶,具體的效果如下圖所示:
我個人覺得挺好玩的打却,所以決定自己實現(xiàn)下這個按鈕,廢話不多說谎倔,先看實現(xiàn)的效果吧:
這個效果看起來和popupwindow差不多柳击,但我是采用自定義view的方式來實現(xiàn),下面說說過程片习。
首先從虎撲的效果可以看到捌肴,它這兩個按鈕時浮在整個界面之上的,所以它需要和FrameLayout結(jié)合使用藕咏,因此我讓它的寬度跟隨屏幕大小状知,高度根據(jù)dpi固定,它的實際尺寸時這樣的:
另外這個view初始化出來我們看到可以分為三塊孽查,背景圓饥悴、圓內(nèi)文字、圓上方數(shù)字盲再,所以正常狀態(tài)下铺坞,只需要在onDraw方法中畫出這三塊內(nèi)容即可。先在初始化方法中將自定義的屬性和畫筆以及初始化數(shù)據(jù)準(zhǔn)備好:
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"};
// 計算背景框改變的長度洲胖,默認(rèn)是三個按鈕
mChangeWidth = (int) (2 * mSmallRadius * 3 + 4 * margin);
}
在onMeasure中測出view的寬度后济榨,根據(jù)寬度計算出背景圓的圓心坐標(biāo)和一些相關(guān)的數(shù)據(jù)值。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
mWidth = getDefaultSize(widthSize, widthMeasureSpec);
setMeasuredDimension(mWidth, mHeight);
// 此時才測出了mWidth值绿映,再計算圓心坐標(biāo)及相關(guān)值
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;
}
因為這里面涉及到點擊按鈕展開和收縮的過程擒滑,所以我定義了如下幾種狀態(tài)腐晾,只有在特定的狀態(tài)下才能進(jìn)行某些操作。
private int mState = STATE_NORMAL;//當(dāng)前展開收縮的狀態(tài)
private boolean mIsRun = false;//是否正在展開或收縮
//正常狀態(tài)
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;
接下來就執(zhí)行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);
}
圓上方的數(shù)字和圓內(nèi)的文字是整個過程中一直存在的藻糖,所以我將這兩個操作放在switch
之外,正常狀態(tài)下繪制圓和之前兩部分文字库车,點擊展開時繪制背景框展開過程和文字巨柒,展開狀態(tài)下再次點擊繪制收縮過程和文字,當(dāng)然在繪制背景框的方法中也需要不斷繪制大圓柠衍,大圓也是一直存在的洋满。
上面的繪制方法:
/**
* 畫背景大圓
* @param canvas
*/
private void drawCircle(Canvas canvas) {
left = cx - mBigRadius;
right = cx + mBigRadius;
canvas.drawCircle(cx, cy, mBigRadius, mBgPaint);
}
/**
* 畫大圓上面表示金幣數(shù)的文字
* @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);
}
/**
* 畫大圓內(nèi)的文字
* @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);
// 繪制第一個按鈕內(nèi)的文字
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);
// 繪制第一個按鈕內(nèi)的文字
canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 20, cy + 15, mTextPaint);
// 繪制第二個按鈕
canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
// 繪制第二個按鈕內(nèi)的文字
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);
// 繪制第一個按鈕內(nèi)的文字
canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint);
// 繪制第二個按鈕
canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint);
// 繪制第二個按鈕內(nèi)的文字
canvas.drawText(mDatas[1], cx - mBigRadius - 3 * mSmallRadius - 2 * margin - 25, cy + 15, mTextPaint);
// 繪制第三個按鈕
canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
// 繪制第三個按鈕內(nèi)的文字
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);
// 繪制第一個按鈕內(nèi)的文字
canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint);
// 繪制第二個按鈕
canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint);
// 繪制第二個按鈕內(nèi)的文字
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);
// 繪制第三個按鈕內(nèi)的文字
canvas.drawText(mDatas[2], cx - mBigRadius - 5 * mSmallRadius - 3 * margin - 34, cy + 15, mTextPaint);
}
drawCircle(canvas);
}
然后是點擊事件的處理,只有觸摸點在大圓內(nèi)時才會觸發(fā)展開或收縮的操作珍坊,點擊小圓時提供了一個接口給外部調(diào)用牺勾。
@Override public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//如果點擊的時候動畫在進(jìn)行,不處理
if (mIsRun) return true;
PointF pointF = new PointF(event.getX(), event.getY());
if (isPointInCircle(pointF, circle, mBigRadius)) { //如果觸摸點在大圓內(nèi)阵漏,根據(jù)彈出方向彈出或者收縮按鈕
if ((mState == STATE_SHRINK || mState == STATE_NORMAL) && !mIsRun) {
//展開
mIsRun = true;//這是必須先設(shè)置true驻民,因為onAnimationStart在onAnimationUpdate之后才調(diào)用
showPopMenu();
} else {
//收縮
mIsRun = true;
hidePopMenu();
}
} else { //觸摸點不在大圓內(nèi)
if (mState == STATE_EXPAND) { //如果是展開狀態(tài)
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);
}
展開和收縮的動畫是改變背景框的寬度屬性的動畫,并監(jiān)聽這個屬性動畫履怯,在寬度值改變的過程中去重新繪制整個view回还。因為一開始我就確定了大圓小圓的半徑和小圓與背景框之間的間距,所以初始化時已經(jīng)計算好了背景框的寬度:
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;
//動畫結(jié)束后設(shè)置狀態(tài)為展開
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;
//動畫結(jié)束后設(shè)置狀態(tài)為收縮
mState = STATE_SHRINK;
}
});
animator.setDuration(500);
animator.start();
}
}
這個過程看起來是彈出或收縮叹洲,實際上寬度值每改變一點懦趋,就將所有的組件重繪一次,只是文字和大圓等內(nèi)容的尺寸及位置都沒有變化疹味,只有背景框的寬度值在變仅叫,所以才有這種效果。
在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();
}
});
大致實現(xiàn)過程就是這樣糙捺,與原始效果還是有點區(qū)別诫咱,我這個還有很多瑕疵,比如文字的位置居中問題洪灯,彈出或收縮時坎缭,小圓內(nèi)的文字的旋轉(zhuǎn)動畫我沒有實現(xiàn)。