Android 開發技巧:使用 Drawable 實現小紅點
在產品的設計中,總難免需要我們開發去實現各種各樣的小紅點,小紅點,小紅點。

導航欄小紅點

側滑菜單項里的小紅點

消息列表的小紅點
通常,我們可能會這樣做:

用一個View實現小紅點,放在相對布局里,設置好內邊距或外邊距,讓它位于圖片的右上角。
或者是給圖片套一個相對布局,設置好圖片的外邊距,然后把表示小紅點的View放在這個相對布局里面的右上角。
這個應該是最簡潔直觀的實現方法。然而,它也有它的局限之處。
比如在我這次的開發當中,一開始只是需要實現如下的界面:


為了省事,我當然是直接用AndroidStudio提供的側滑菜單的模板了,然后再稍作改動,設置一下導航欄的按鈕圖標和內容布局,寫一下側滑Header的布局,再寫一下側滑菜單的menu.xml文件,就完成了。
在完成了這些,其他功能開發到一半的時候才說要在這兩個界面增加小紅點。然而,我們的標題欄用的是toolbar,默認對于這個導航圖標的設置是只能通過 toolbar.setNavigationIcon(Drawable icon) 或 toolbar.setNavigationIcon(int resId) 來設置一個圖片上去的,并不能在里面添加一個小紅點的View。
另外,我們的側滑菜單,也是通過在menu資源文件夾里通過如下方式來定義的:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_wallet"
android:icon="@drawable/icon_menu_wallet"
android:title="@string/menu_my_wallet"/>
<item
android:id="@+id/nav_plate"
android:icon="@drawable/icon_menu_plate"
android:title="@string/menu_my_vehicle"/>
<!--其他菜單項略-->
</menu>
它也只是指定圖標和文字,并不能指定小紅點。
如果說只為實現這兩個小紅點,就要自己去做toolbar及側滑菜單的自定義實現,改一大堆代碼,從時間成本上考慮,眼前都要過年了,我肯定是不樂意的。好在發現它們兩個都可以獲取及設置drawable,那就有辦法了。
思路如下,實現一個Drawable,在它里面套一層原來的Drawable,并且繪制出我們的小紅點。好像很簡單?support庫里的 TintAwareDrawable 就是這么做的。
接下來思考一下我們要實現的具體功能。
首先,前面的小紅點,如果你注意觀察會發現,它們的位置不是都以圖片的右上角為中心點的。
比如導航欄的小紅點左邊緣是與圖標右邊緣對齊的:

消息中心是小紅點的右邊緣與圖標的右邊緣對齊的:

另外,我們還需要一個開關,設置是否顯示小紅點。
最終,代碼實現如下:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.res.ResourcesCompat;
import android.view.Gravity;
public class RedPointDrawable extends Drawable {
private Drawable mDrawable;
private boolean mShowRedPoint;
private Paint mPaint;
private int mRadius;
private int mGravity = Gravity.CENTER;
public RedPointDrawable(Context context, Drawable origin) {
mDrawable = origin;// 原來的drawable
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(Color.RED);
mRadius = context.getResources().getDimensionPixelSize(R.dimen.red_point_radius_small);//小紅點半徑
}
public void setColor(int color) {
mPaint.setColor(color);
}
public void setShowRedPoint(boolean showRedPoint) {
mShowRedPoint = showRedPoint;
invalidateSelf();
}
public void setRadius(int radius) {
this.mRadius = radius;
}
public void setGravity(int gravity) {
this.mGravity = gravity;
}
@Override
public void draw(@NonNull Canvas canvas)
mDrawable.draw(canvas);//先繪制原圖標
if (mShowRedPoint) {
// 獲取原圖標的右上角坐標
int cx = getBounds().right;
int cy = getBounds().top;
// 計算我們的小紅點的坐標
if ((Gravity.LEFT & mGravity) == Gravity.LEFT) {
cx -= mRadius;
} else if ((Gravity.RIGHT & mGravity) == Gravity.RIGHT) {
cx += mRadius;
}
if ((Gravity.TOP & mGravity) == Gravity.TOP) {
cy -= mRadius;
} else if ((Gravity.BOTTOM & mGravity) == Gravity.BOTTOM) {
cy += mRadius;
}
canvas.drawCircle(cx, cy, mRadius, mPaint);//繪制小紅點
}
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
mDrawable.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
mDrawable.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return mDrawable.getOpacity();
}
@Override
public int getIntrinsicHeight() {
return mDrawable.getIntrinsicHeight();//它的高度使用原來的高度
}
@Override
public int getIntrinsicWidth() {
return mDrawable.getIntrinsicWidth();//它的寬度使用原來的寬度
}
@Override
public void setBounds(@NonNull Rect bounds) {
super.setBounds(bounds);
mDrawable.setBounds(bounds);
}
@Override
public void setBounds(int left, int top, int right, int bottom) {
super.setBounds(left, top, right, bottom);
mDrawable.setBounds(left, top, right, bottom);
}
public static RedPointDrawable wrap(Context context, Drawable drawable) {
// 把原來的Drawable包裝為一個小紅點的Drawable
if (drawable instanceof RedPointDrawable) {
return (RedPointDrawable) drawable;
}
return new RedPointDrawable(context, drawable);
}
}
下面就可以使用它來給我們的導航欄圖標設置小紅點了。設置導航欄圖標的代碼改為如下:
final RedPointDrawable icon = new RedPointDrawable(this, ResourcesCompat.getDrawable(getResources(), R.drawable.icon_user, null));
icon.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL);
toolbar.setNavigationIcon(icon);
// 把drawable添加到我們的成員變量中去,以便后面直接對它進行設置
//mRedPointView.addRedPointDrawable(redPointDrawable);
然后我們可以把這個icon給保存到成員變量里,通過調用這個drawable的 setShowRedPoint(boolean) 就可以設置顯示及隱藏了。
然后我們還要獲取側滑菜單消息中心的drawable,給它也設置一下:
private void initForMessageCenterIcon(NavigationView navigationView) {
Menu menu = navigationView.getMenu();
int size = menu.size();
for (int i = 0; i < size; i++) {
MenuItem item = menu.getItem(i);
if (item.getItemId() == R.id.nav_message) {
RedPointDrawable redPointDrawable = RedPointDrawable.wrap(this, item.getIcon());
redPointDrawable.setGravity(Gravity.LEFT);
item.setIcon(redPointDrawable);
// 把drawable添加到我們的成員變量中去,以便后面直接對它進行設置
//mRedPointView.addRedPointDrawable(redPointDrawable);
}
}
}
總結
在需要小紅點時,固然可以通過再寫多一個View來實現,這是在可以自定義布局時的一種通用的方式。但如果不想修改布局時,通過Drawable則可以做到對布局文件的零入侵,達到四兩撥千斤的效果,不過需要注意Drawable的大小是否仍在控件所允許顯示的范圍內。
來自:http://www.jianshu.com/p/e500204a41b2