开源项目Plaid学习(七)FourThreeImageView
前言
上一期介绍了ForegroundImageView,这一期介绍其子类FourThreeImageView和子类的子类BadgedFourThreeImageView。
FourThreeImageView
源码
/**
* A extension of ForegroundImageView that is always 4:3 aspect ratio.
*/
public class FourThreeImageView extends ForegroundImageView {
public FourThreeImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int fourThreeHeight = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthSpec) * 3 / 4,
MeasureSpec.EXACTLY);
super.onMeasure(widthSpec, fourThreeHeight);
}
}
非常短的代码,目的也非常简单:强制4:3尺寸。具体来说就是把高度设定为宽度的3/4.实现就是改写onMeasure方法。
BadgedFourThreeImageView
源码
/**
* A view group that draws a badge drawable on top of it's contents.
*/
public class BadgedFourThreeImageView extends FourThreeImageView {
private Drawable badge;
private boolean drawBadge;
private boolean badgeBoundsSet = false;
private int badgeGravity;
private int badgePadding;
public BadgedFourThreeImageView(Context context, AttributeSet attrs) {
super(context, attrs);
badge = new GifBadge(context);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BadgedImageView, 0, 0);
badgeGravity = a.getInt(R.styleable.BadgedImageView_badgeGravity, Gravity.END | Gravity
.BOTTOM);
badgePadding = a.getDimensionPixelSize(R.styleable.BadgedImageView_badgePadding, 0);
a.recycle();
}
public void showBadge(boolean show) {
drawBadge = show;
}
public void setBadgeColor(@ColorInt int color) {
badge.setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (drawBadge) {
if (!badgeBoundsSet) {
layoutBadge();
}
badge.draw(canvas);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
layoutBadge();
}
private void layoutBadge() {
Rect badgeBounds = badge.getBounds();
Gravity.apply(badgeGravity,
badge.getIntrinsicWidth(),
badge.getIntrinsicHeight(),
new Rect(0, 0, getWidth(), getHeight()),
badgePadding,
badgePadding,
badgeBounds);
badge.setBounds(badgeBounds);
badgeBoundsSet = true;
}
/**
* A drawable for indicating that an image is animated
*/
private static class GifBadge extends Drawable {
private static final String GIF = "GIF";
private static final int TEXT_SIZE = 12; // sp
private static final int PADDING = 4; // dp
private static final int CORNER_RADIUS = 2; // dp
private static final int BACKGROUND_COLOR = Color.WHITE;
private static final String TYPEFACE = "sans-serif-black";
private static final int TYPEFACE_STYLE = Typeface.NORMAL;
private static Bitmap bitmap;
private static int width;
private static int height;
private final Paint paint;
GifBadge(Context context) {
if (bitmap == null) {
final DisplayMetrics dm = context.getResources().getDisplayMetrics();
final float density = dm.density;
final float scaledDensity = dm.scaledDensity;
final TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint
.SUBPIXEL_TEXT_FLAG);
textPaint.setTypeface(Typeface.create(TYPEFACE, TYPEFACE_STYLE));
textPaint.setTextSize(TEXT_SIZE * scaledDensity);
final float padding = PADDING * density;
final float cornerRadius = CORNER_RADIUS * density;
final Rect textBounds = new Rect();
textPaint.getTextBounds(GIF, 0, GIF.length(), textBounds);
height = (int) (padding + textBounds.height() + padding);
width = (int) (padding + textBounds.width() + padding);
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setHasAlpha(true);
final Canvas canvas = new Canvas(bitmap);
final Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
backgroundPaint.setColor(BACKGROUND_COLOR);
canvas.drawRoundRect(0, 0, width, height, cornerRadius, cornerRadius,
backgroundPaint);
// punch out the word 'GIF', leaving transparency
textPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawText(GIF, padding, height - padding, textPaint);
}
paint = new Paint();
}
@Override
public int getIntrinsicWidth() {
return width;
}
@Override
public int getIntrinsicHeight() {
return height;
}
@Override
public void draw(Canvas canvas) {
canvas.drawBitmap(bitmap, getBounds().left, getBounds().top, paint);
}
@Override
public void setAlpha(int alpha) {
// ignored
}
@Override
public void setColorFilter(ColorFilter cf) {
paint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return 0;
}
}
}
使用了自定义属性attrs_badge_image_view.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BadgedImageView">
<attr name="badgeGravity">
<!-- Push object to the top of its container, not changing its size. -->
<flag name="top" value="0x30" />
<!-- Push object to the bottom of its container, not changing its size. -->
<flag name="bottom" value="0x50" />
<!-- Push object to the left of its container, not changing its size. -->
<flag name="left" value="0x03" />
<!-- Push object to the right of its container, not changing its size. -->
<flag name="right" value="0x05" />
<!-- Push object to the beginning of its container, not changing its size. -->
<flag name="start" value="0x00800003" />
<!-- Push object to the end of its container, not changing its size. -->
<flag name="end" value="0x00800005" />
</attr>
<attr name="badgePadding" format="dimension|reference"/>
</declare-styleable>
</resources>
这里的自定义属性和以往的有一点不同,即多了flag项目。观察:0x30=0b_0011_0000,0x50=0b_0101_0000,之后的都类似。很多时候我们都会用到Gravity的或运算,例如Gravity.left|Gravity.bottom,其实也就是其value的或,因此将某一对矛盾的属性设置到特定的比特位上,这样相互之间就不会干扰。当然,这里这么设置值自然有其道理,具体还得参考Gravity的源码。
自定义属性就两个,一个Gravity,另一个padding,两者一起来规定前景图片的具体位置和尺寸。
此外,这个类里面还定义了一个GifBadge,就是把“Gif”字样转为Drawable类型用于前景,表面该图片实际上是Gif的静态图。具体实现也牵扯了很多细节,简单来说就是在bitmap画个背景和Textview组成Drawable。根据这个类,可以很方便地举一反三得到通用可以设置字符串内容,字体,字的颜色,字的大小,背景的一个类,估计也就用在需要前景字体的图片上了。
还是回到源码。前面没太多好说的。PorterDuff.Mode有非常多模式,这里有比较详细的解释。简单而言就是图像重叠的处理。
这里使用SRC_IN,来实现背景色变。GIF则是镂空的,因为Clear模式会造成透明背景,在GifBadge里面textPaint已经设置。
layoutBadge里面,自定义的Gravity如何与源码的Gravity互动比较值得探究:
/** Constant indicating that no gravity has been set **/
public static final int NO_GRAVITY = 0x0000;
/** Raw bit indicating the gravity for an axis has been specified. */
public static final int AXIS_SPECIFIED = 0x0001;
/** Raw bit controlling how the left/top edge is placed. */
public static final int AXIS_PULL_BEFORE = 0x0002;
/** Raw bit controlling how the right/bottom edge is placed. */
public static final int AXIS_PULL_AFTER = 0x0004;
/** Raw bit controlling whether the right/bottom edge is clipped to its
* container, based on the gravity direction being applied. */
public static final int AXIS_CLIP = 0x0008;
/** Bits defining the horizontal axis. */
public static final int AXIS_X_SHIFT = 0;
/** Bits defining the vertical axis. */
public static final int AXIS_Y_SHIFT = 4;
/** Push object to the top of its container, not changing its size. */
public static final int TOP = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;
/** Push object to the bottom of its container, not changing its size. */
public static final int BOTTOM = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;
/** Push object to the left of its container, not changing its size. */
public static final int LEFT = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_X_SHIFT;
/** Push object to the right of its container, not changing its size. */
public static final int RIGHT = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_X_SHIFT;
/** Place object in the vertical center of its container, not changing its
* size. */
public static final int CENTER_VERTICAL = AXIS_SPECIFIED<<AXIS_Y_SHIFT;
/** Grow the vertical size of the object if needed so it completely fills
* its container. */
public static final int FILL_VERTICAL = TOP|BOTTOM;
/** Place object in the horizontal center of its container, not changing its
* size. */
public static final int CENTER_HORIZONTAL = AXIS_SPECIFIED<<AXIS_X_SHIFT;
/** Grow the horizontal size of the object if needed so it completely fills
* its container. */
public static final int FILL_HORIZONTAL = LEFT|RIGHT;
/** Place the object in the center of its container in both the vertical
* and horizontal axis, not changing its size. */
public static final int CENTER = CENTER_VERTICAL|CENTER_HORIZONTAL;
/** Grow the horizontal and vertical size of the object if needed so it
* completely fills its container. */
public static final int FILL = FILL_VERTICAL|FILL_HORIZONTAL;
/** Flag to clip the edges of the object to its container along the
* vertical axis. */
public static final int CLIP_VERTICAL = AXIS_CLIP<<AXIS_Y_SHIFT;
/** Flag to clip the edges of the object to its container along the
* horizontal axis. */
public static final int CLIP_HORIZONTAL = AXIS_CLIP<<AXIS_X_SHIFT;
/** Raw bit controlling whether the layout direction is relative or not (START/END instead of
* absolute LEFT/RIGHT).
*/
public static final int RELATIVE_LAYOUT_DIRECTION = 0x00800000;
/**
* Binary mask to get the absolute horizontal gravity of a gravity.
*/
public static final int HORIZONTAL_GRAVITY_MASK = (AXIS_SPECIFIED |
AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_X_SHIFT;
/**
* Binary mask to get the vertical gravity of a gravity.
*/
public static final int VERTICAL_GRAVITY_MASK = (AXIS_SPECIFIED |
AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_Y_SHIFT;
/** Special constant to enable clipping to an overall display along the
* vertical dimension. This is not applied by default by
* {@link #apply(int, int, int, Rect, int, int, Rect)}; you must do so
* yourself by calling {@link #applyDisplay}.
*/
public static final int DISPLAY_CLIP_VERTICAL = 0x10000000;
/** Special constant to enable clipping to an overall display along the
* horizontal dimension. This is not applied by default by
* {@link #apply(int, int, int, Rect, int, int, Rect)}; you must do so
* yourself by calling {@link #applyDisplay}.
*/
public static final int DISPLAY_CLIP_HORIZONTAL = 0x01000000;
/** Push object to x-axis position at the start of its container, not changing its size. */
public static final int START = RELATIVE_LAYOUT_DIRECTION | LEFT;
/** Push object to x-axis position at the end of its container, not changing its size. */
public static final int END = RELATIVE_LAYOUT_DIRECTION | RIGHT;
/**
* Binary mask for the horizontal gravity and script specific direction bit.
*/
public static final int RELATIVE_HORIZONTAL_GRAVITY_MASK = START | END;
/**
* Apply a gravity constant to an object.
*
* @param gravity The desired placement of the object, as defined by the
* constants in this class.
* @param w The horizontal size of the object.
* @param h The vertical size of the object.
* @param container The frame of the containing space, in which the object
* will be placed. Should be large enough to contain the
* width and height of the object.
* @param xAdj Offset to apply to the X axis. If gravity is LEFT this
* pushes it to the right; if gravity is RIGHT it pushes it to
* the left; if gravity is CENTER_HORIZONTAL it pushes it to the
* right or left; otherwise it is ignored.
* @param yAdj Offset to apply to the Y axis. If gravity is TOP this pushes
* it down; if gravity is BOTTOM it pushes it up; if gravity is
* CENTER_VERTICAL it pushes it down or up; otherwise it is
* ignored.
* @param outRect Receives the computed frame of the object in its
* container.
*/
public static void apply(int gravity, int w, int h, Rect container,
int xAdj, int yAdj, Rect outRect) {
switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT)) {
case 0:
outRect.left = container.left
+ ((container.right - container.left - w)/2) + xAdj;
outRect.right = outRect.left + w;
if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
== (AXIS_CLIP<<AXIS_X_SHIFT)) {
if (outRect.left < container.left) {
outRect.left = container.left;
}
if (outRect.right > container.right) {
outRect.right = container.right;
}
}
break;
case AXIS_PULL_BEFORE<<AXIS_X_SHIFT:
outRect.left = container.left + xAdj;
outRect.right = outRect.left + w;
if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
== (AXIS_CLIP<<AXIS_X_SHIFT)) {
if (outRect.right > container.right) {
outRect.right = container.right;
}
}
break;
case AXIS_PULL_AFTER<<AXIS_X_SHIFT:
outRect.right = container.right - xAdj;
outRect.left = outRect.right - w;
if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
== (AXIS_CLIP<<AXIS_X_SHIFT)) {
if (outRect.left < container.left) {
outRect.left = container.left;
}
}
break;
default:
outRect.left = container.left + xAdj;
outRect.right = container.right + xAdj;
break;
}
switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_Y_SHIFT)) {
case 0:
outRect.top = container.top
+ ((container.bottom - container.top - h)/2) + yAdj;
outRect.bottom = outRect.top + h;
if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
== (AXIS_CLIP<<AXIS_Y_SHIFT)) {
if (outRect.top < container.top) {
outRect.top = container.top;
}
if (outRect.bottom > container.bottom) {
outRect.bottom = container.bottom;
}
}
break;
case AXIS_PULL_BEFORE<<AXIS_Y_SHIFT:
outRect.top = container.top + yAdj;
outRect.bottom = outRect.top + h;
if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
== (AXIS_CLIP<<AXIS_Y_SHIFT)) {
if (outRect.bottom > container.bottom) {
outRect.bottom = container.bottom;
}
}
break;
case AXIS_PULL_AFTER<<AXIS_Y_SHIFT:
outRect.bottom = container.bottom - yAdj;
outRect.top = outRect.bottom - h;
if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
== (AXIS_CLIP<<AXIS_Y_SHIFT)) {
if (outRect.top < container.top) {
outRect.top = container.top;
}
}
break;
default:
outRect.top = container.top + yAdj;
outRect.bottom = container.bottom + yAdj;
break;
}
}
以默认的right|bottom为例,数值为0x55.然后到apply函数里面,因为AXIS_PULL_BEFORE|AXIS_PULL_AFTER这个值就是0x6,就算左移4位也还是0x60,那么和参数gravity与,结果第一个switch是0x4,第二个结果0x40,都和AXIS_PULL_AFTER一致。观察代码,也确实是计算了padding。为什么呢?怎么办到的?
从apply的源码分析,其实Gravity这个属性主要是分成两个16进制位来看,低位决定X轴也就是左右,高位决定Y轴也就是上下,因此自定义的值,关系到左右的只在低位有值,关系到上下的也只在高位有值。当然可以一起有值,不过代码还是分开处理。
其实两个位上的操作很像,都是和对应的6与。6有是0b110,其实就是AXIS_PULL_BEFORE|AXIS_PULL_AFTER,按照注释,AXIS_PULL_BEFORE代表top/left,AXIS_PULL_AFTER代表bottom/right。那center呢?其实center是AXIS_SPECIFIED管的。具体到switch的case,0的意思就是居中,AXIS_PULL_BEFORE就是之前解释的top/left,AXIS_PULL_AFTER就是bottom/right。当然还有优先级更高的一层clip判断。
开始我还以为是作者自己搞了一套Gravity的值,结果原来还是和源码里的值一致。只是从源码看不出来,START/END是怎么体现的?NONE和AXIS_SPECIFIED又如何区分?
虽然还有疑问,不过也算是搞明白怎么自定义Gravity了。
实际效果
Plaid里面使用该View的实际设置是这样的:
<io.plaidapp.ui.widget.BadgedFourThreeImageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/shot"
android:layout_width="match_parent"
android:layout_height="0dp"
android:elevation="@dimen/z_card"
android:stateListAnimator="@animator/raise"
android:foreground="@drawable/mid_grey_ripple"
app:badgeGravity="end|bottom"
app:badgePadding="@dimen/padding_normal" />
也就是说,我前面提到过ForegroundImageView的前景图片会扩充全图,其实是故意的,因为这里设置的前景其实是一个ripple Drawable,使得图片对点击产生ripple效果。而这里则是加一个GIF的badge而已。
换种想法,利用FrameLayout结合foreground属性也可以实现这个效果。Anyway,学到就是赚到。