前言
上次介绍了一系列的TextView,这回来介绍一系列ImageView,大致关系是ImageView->ForegroundImageView->FourThreeImageView->BadgedFourThreeImageView。
源码
/**
* An extension to {@link ImageView} which has a foreground drawable.
*/
public class ForegroundImageView extends ImageView {
private Drawable foreground;
public ForegroundImageView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundView);
final Drawable d = a.getDrawable(R.styleable.ForegroundView_android_foreground);
if (d != null) {
setForeground(d);
}
a.recycle();
setOutlineProvider(ViewOutlineProvider.BOUNDS);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (foreground != null) {
foreground.setBounds(0, 0, w, h);
}
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || (who == foreground);
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (foreground != null) foreground.jumpToCurrentState();
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (foreground != null && foreground.isStateful()) {
foreground.setState(getDrawableState());
}
}
/**
* Returns the drawable used as the foreground of this view. The
* foreground drawable, if non-null, is always drawn on top of the children.
*
* @return A Drawable or null if no foreground was set.
*/
public Drawable getForeground() {
return foreground;
}
/**
* Supply a Drawable that is to be rendered on top of the contents of this ImageView
*
* @param drawable The Drawable to be drawn on top of the ImageView
*/
public void setForeground(Drawable drawable) {
if (foreground != drawable) {
if (foreground != null) {
foreground.setCallback(null);
unscheduleDrawable(foreground);
}
foreground = drawable;
if (foreground != null) {
foreground.setBounds(0, 0, getWidth(), getHeight());
setWillNotDraw(false);
foreground.setCallback(this);
if (foreground.isStateful()) {
foreground.setState(getDrawableState());
}
} else {
setWillNotDraw(true);
}
invalidate();
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (foreground != null) {
foreground.draw(canvas);
}
}
@Override
public void drawableHotspotChanged(float x, float y) {
super.drawableHotspotChanged(x, y);
if (foreground != null) {
foreground.setHotspot(x, y);
}
}
}
很明显使用了一个自定义属性attrs_foreground_view.xml:
<resources>
<declare-styleable name="ForegroundView">
<attr name="android:foreground" />
<attr name="android:foregroundInsidePadding" />
<attr name="android:foregroundGravity" />
</declare-styleable>
</resources>
前缀是android,说明这些属性已经在系统里面定义好了,可以直接拿出来用而不用再定义format。foreground这个属性就是foreground drawable。
这个控件的目的非常明确,也就是在原有的图片之前加一个前景图片。
然后就从代码顺序来进行拓展和学习。
setOutlineProvider(ViewOutlineProvider.BOUNDS);
这个是在构造器里的最后一行代码。对于ViewOutlineProvider,官方的解释是
public abstract class ViewOutlineProvider
extends Object
java.lang.Object
↳ android.view.ViewOutlineProvider
Interface by which a View builds its Outline , used for shadow casting and clipping.
=_=好吧那什么又是outline呢?
public final class Outline
extends Object
java.lang.Object
↳ android.graphics.Outline
Defines a simple shape, used for bounding graphical regions.
Can be computed for a View, or computed by a Drawable, to drive the shape of shadows cast by a View, or to clip the contents of the View.
Outline的中文翻译就是轮廓,可以用来裁剪和投影。再回到ViewOutlineProvider,有三种方式:
Default outline provider for Views, which queries the Outline from the View's background, or generates a 0 alpha, rectangular Outline the size of the View if a background isn't present.
BOUNDS
Maintains the outline of the View to match its rectangular bounds, at 1.0f alpha.
PADDED_BOUNDS
Maintains the outline of the View to match its rectangular padded bounds, at 1.0f alpha.
BACKGROUND是默认类型,而后面两个其实差不多,区别只是padding。这三个是默认自带的,如果有需要,还可以自己定义一个ViewOutlineProvider,只需override掉getOutline方法。
比如安卓SDK源码里面BACKGROUND的实现:
public static final ViewOutlineProvider BACKGROUND = new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
Drawable background = view.getBackground();
if (background != null) {
background.getOutline(outline);
} else {
outline.setRect(0, 0, view.getWidth(), view.getHeight());
outline.setAlpha(0.0f);
}
}
};
至于Bounds的实现就是else那一小段。也就是说直接取view的矩形区域作为outline。ImageView确实有getBackground()方法不过很少使用,是怕万一设置了Background导致outline不对?这也只是我的猜测。
onSizeChanged
这个方法是从View一路继承下来的,View那里啥也不干,就是给后面继承的;比较有意思的是ImageView并没有重写这个方法,其尺寸是和作为来源的图片有关的。在这里,其实也就是把foreground的bounds设置得和本身一样。
ImageView的源码里面并没有调用任何onSizeChanged方法,也就是说其本身并不会改变尺寸,而这个函数在这里重写,是为了在外界强行改变尺寸时,确保foreground的边界也随之变化。
hasOverlappingRendering
boolean hasOverlappingRendering ()
Returns whether this View has content which overlaps.
This function, intended to be overridden by specific View types, is an optimization when alpha is set on a view. If rendering overlaps in a view with alpha < 1, that view is drawn to an offscreen buffer and then composited into place, which can be expensive. If the view has no overlapping rendering, the view can draw each primitive with the appropriate alpha value directly. An example of overlapping rendering is a TextView with a background image, such as a Button. An example of non-overlapping rendering is a TextView with no background, or an ImageView with only the foreground image. The default implementation returns true; subclasses should override if they have cases which can be optimized.
The current implementation of the saveLayer and saveLayerAlpha methods in Canvas
necessitates that a View return true if it uses the methods internally without passing the CLIP_TO_LAYER_SAVE_FLAG.
Note: The return value of this method is ignored if forceHasOverlappingRendering(boolean)
has been called on this view.
意思就是,假如有重叠的情况要不要考虑重叠效果。这里应该是foreground的会直接遮盖后面的内容,就不必再考虑重叠效果了。
verifyDrawable
boolean verifyDrawable (Drawable dr)
If your view subclass is displaying its own Drawable objects, it should override this function and return true for any Drawable it is displaying. This allows animations for those drawables to be scheduled.
Be sure to call through to the super class when overriding this function.
意思就是假如显示自己的drawable就应该重写该方法,调用super方法并返回true。
看了一下源码,从View->ImageView都有这个方法,而且挺短的:
// from View
protected boolean verifyDrawable(@NonNull Drawable who) {
// Avoid verifying the scroll bar drawable so that we don't end up in
// an invalidation loop. This effectively prevents the scroll bar
// drawable from triggering invalidations and scheduling runnables.
return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who);
}
// from ImageView
@Override
protected boolean verifyDrawable(@NonNull Drawable dr) {
return mDrawable == dr || super.verifyDrawable(dr);
}
首先要搞明白这个函数什么时候调用,就是在scheduleDrawable(),unscheduleDrawable()还有invalidateDrawable()时调用,其功能和名字一样。
View里面验证其是不是前景或者背景,ImageView里面加上是不是源的验证。
同时我们也可以看出,其实在View里面已经有了ForegroundInfo这个东西,里面带了Drawable,而且从API1就有。那么为什么要大费周章再弄一个呢?而且也支持Gravity。
光从这一个自定义View来看,确实找不到很好的理由。可能之后系列的自定义View会继续扩展吧。不过也可能作者是真没想那么多也说不准……
jumpDrawablesToCurrentState & drawableStateChanged
void jumpDrawablesToCurrentState ()
Call Drawable.jumpToCurrentState()
on all Drawable objects associated with this view.
Also calls jumpToCurrentState()
if there is a StateListAnimator attached to this view.
就是说,Drawable可能会有不同的state也就是状态,这个就是把所有相关的Drawable跳到当前状态。这里只需要再把foreground跳一下就行。drawableStateChanged也是同理。
setForeground
核心方法。首先判断是不是已经设置好了,然后取消之前的一些回调和schedule;再判断新的Drawable,为空则跳过draw步骤;否则,设置边界,设置会调用draw,然后设置一下回调和状态。
foreground.setCallback(this)这一句,this其实指代的是ImageView里面的Callback,其实也就是一个接口,实现invalidateDrawable,scheduleDrawable和unscheduleDrawable三个方法。其实这个回调接口在View里面就有了:public class View implements Drawable.Callback……
invalidate在字面上的意思是“废止”,有点不太好理解,其实就是告诉系统有东西变化了,让系统来重画。
drawableHotspotChanged
Hotspot字面意思是热点,据说是用来实现ripple效果的。这里也对前景设置一下就好了。
实际效果
对于本控件,因为没有设置foreground的具体尺寸,因此会扩充到整个控件大小,真正的“前景”。而且由于设置了不重叠模式,前景会挡住后面的内容。总而言之,暂时来看用处并不是非常大,急需一个可以设置foreground尺寸的功能。
对于之前谈到过的View自带foreground,我试了一下,ImageView不能显示而FrameLayout可以。当然FrameLayout是通过设置背景再设置前景达到这样的目的,本质上来说并不是ImageView。
所以说,作者写这个控件确实是有原因的。当然这个只是初步,在下一期将介绍Plaid真正用到的BadgedFourThreeImageView。