Android 开发技巧:使用 Drawable 实现小红点

ann2blog 8年前
   <p>在产品的设计中,总难免需要我们开发去实现各种各样的小红点,小红点,小红点。</p>    <p><img src="https://simg.open-open.com/show/ede71fe0baff1cb4e1ead6735445f796.jpg"></p>    <p>导航栏小红点</p>    <p><img src="https://simg.open-open.com/show/16db9d22fdefec32cec73e975a10bd86.jpg"></p>    <p>侧滑菜单项里的小红点</p>    <p><img src="https://simg.open-open.com/show/6ec6a44b952bd0d1662e742f449535d1.jpg"></p>    <p>消息列表的小红点</p>    <p>通常,我们可能会这样做:</p>    <p><img src="https://simg.open-open.com/show/7cfc10c3512c4dee110da6626cd54832.jpg"></p>    <p>用一个View实现小红点,放在相对布局里,设置好内边距或外边距,让它位于图片的右上角。</p>    <p>或者是给图片套一个相对布局,设置好图片的外边距,然后把表示小红点的View放在这个相对布局里面的右上角。</p>    <p>这个应该是最简洁直观的实现方法。然而,它也有它的局限之处。</p>    <p>比如在我这次的开发当中,一开始只是需要实现如下的界面:</p>    <p><img src="https://simg.open-open.com/show/60cd1c8bc18bbc3ed9d1af24ded037dc.jpg"></p>    <p><img src="https://simg.open-open.com/show/8f62de3dc60fd3d8a5701adb2ae8f0e1.jpg"></p>    <p>为了省事,我当然是直接用AndroidStudio提供的侧滑菜单的模板了,然后再稍作改动,设置一下导航栏的按钮图标和内容布局,写一下侧滑Header的布局,再写一下侧滑菜单的menu.xml文件,就完成了。</p>    <p>在完成了这些,其他功能开发到一半的时候才说要在这两个界面增加小红点。然而,我们的标题栏用的是toolbar,默认对于这个导航图标的设置是只能通过 toolbar.setNavigationIcon(Drawable icon) 或 toolbar.setNavigationIcon(int resId) 来设置一个图片上去的,并不能在里面添加一个小红点的View。</p>    <p>另外,我们的侧滑菜单,也是通过在menu资源文件夹里通过如下方式来定义的:</p>    <pre>  <code class="language-xml"><?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></code></pre>    <p>它也只是指定图标和文字,并不能指定小红点。</p>    <p>如果说只为实现这两个小红点,就要自己去做toolbar及侧滑菜单的自定义实现,改一大堆代码,从时间成本上考虑,眼前都要过年了,我肯定是不乐意的。好在发现它们两个都可以获取及设置drawable,那就有办法了。</p>    <p>思路如下,实现一个Drawable,在它里面套一层原来的Drawable,并且绘制出我们的小红点。好像很简单?support库里的 TintAwareDrawable 就是这么做的。</p>    <p>接下来思考一下我们要实现的具体功能。</p>    <p>首先,前面的小红点,如果你注意观察会发现,它们的位置不是都以图片的右上角为中心点的。</p>    <p>比如导航栏的小红点左边缘是与图标右边缘对齐的:</p>    <p><img src="https://simg.open-open.com/show/f094da3ce193b793253a5651faefecfe.jpg"></p>    <p>消息中心是小红点的右边缘与图标的右边缘对齐的:</p>    <p><img src="https://simg.open-open.com/show/ebe796fede5f703ffd267691f9904245.jpg"></p>    <p>另外,我们还需要一个开关,设置是否显示小红点。</p>    <p>最终,代码实现如下:</p>    <pre>  <code class="language-java">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);      }  }</code></pre>    <p>下面就可以使用它来给我们的导航栏图标设置小红点了。设置导航栏图标的代码改为如下:</p>    <pre>  <code class="language-java">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);</code></pre>    <p>然后我们可以把这个icon给保存到成员变量里,通过调用这个drawable的 setShowRedPoint(boolean) 就可以设置显示及隐藏了。</p>    <p>然后我们还要获取侧滑菜单消息中心的drawable,给它也设置一下:</p>    <pre>  <code class="language-java">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);              }          }      }</code></pre>    <h2>总结</h2>    <p>在需要小红点时,固然可以通过再写多一个View来实现,这是在可以自定义布局时的一种通用的方式。但如果不想修改布局时,通过Drawable则可以做到对布局文件的零入侵,达到四两拨千斤的效果,不过需要注意Drawable的大小是否仍在控件所允许显示的范围内。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/e500204a41b2</p>    <p> </p>