富文本 SpannableString 支持网络图
实现思路: 在文本前加空格占位,使用默认图填充,下载网络图,下载完成时替换默认图。
CenterVerticalImageSpan 实现图片在文字中居中效果。
记得网络图回来后,还要调用 textView.setText(spannableString, TextView.BufferType.SPANNABLE);
TextView textView = (TextView)weakView;
SpannableString spannableString = new SpannableString(" " + textView.getText());
ImageSpan defaultSpan = new CenterVerticalImageSpan(context, R.drawable.ic_default);
spannableString.setSpan(defaultSpan,0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString, TextView.BufferType.SPANNABLE);
String imageUrl = imageSpan.getString("image"); // 图片链接
if (!TextUtils.isEmpty(imageUrl)) {
RemoteImageView remoteImageView = new RemoteImageView(context);
remoteImageView.setLoadOriginal(true);
remoteImageView.setImageLoadListener(new PainterImageLoadListener() {
@Override
public boolean onHandleResourceReady(ImageView imageView, Object drawable) {
int start = spannableString.getSpanStart(defaultSpan);
int end = spannableString.getSpanEnd(defaultSpan);
if (start != -1 && end != -1) {
if (drawable instanceof Drawable) {
((Drawable) drawable).setBounds(0, 0, ((Drawable) drawable).getIntrinsicWidth(), ((Drawable) drawable).getIntrinsicHeight());
spannableString.removeSpan(defaultSpan);
spannableString.setSpan(new CenterVerticalImageSpan((Drawable) drawable), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString, TextView.BufferType.SPANNABLE);
textView.requestLayout();
}
}
return false;
}
@Override
public boolean onHandleLoadFailed(ImageView imageView) {
return false;
}
});
remoteImageView.load(imageUrl);
}
图片居中: CenterVerticalImageSpan.java
public class CenterVerticalImageSpan extends ImageSpan {
public CenterVerticalImageSpan(Context context, int resourceId) {
super(context, resourceId);
}
public CenterVerticalImageSpan(Drawable drawable) {
super(drawable);
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end,
Paint.FontMetricsInt fontMetricsInt) {
try {
Drawable drawable = getDrawable();
Rect rect = drawable.getBounds();
if (fontMetricsInt != null) {
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.bottom - fmPaint.top;
int drHeight = rect.bottom - rect.top;
//对于这里我表示,我不知道为啥是这样。不应该是fontHeight/2?但是只有fontHeight/4才能对齐
//难道是因为TextView的draw的时候top和bottom是大于实际的?具体请看下图
//所以fontHeight/4是去除偏差?
int top = drHeight / 2 - fontHeight / 4;
int bottom = drHeight / 2 + fontHeight / 4;
fontMetricsInt.ascent = -bottom;
fontMetricsInt.top = -bottom;
fontMetricsInt.bottom = top;
fontMetricsInt.descent = top;
}
return rect.right;
} catch (Exception e) {
return 20;
}
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, Paint paint) {
try {
Drawable drawable = getDrawable();
canvas.save();
int transY;
//获得将要显示的文本高度-图片高度除2等居中位置+top(换行情况)
transY = ((bottom - top) - drawable.getBounds().bottom) / 2 + top;
canvas.translate(x, transY);
drawable.draw(canvas);
canvas.restore();
} catch (Exception e) {
}
}
}
以上代码有点问题,就是图片给多大,就会显示多大,没有按照图片宽高比做自适应。
需要自适应时,只需外部传入宽高(realWidth, realHeight),直接设置给drawable
伪代码如下:
((Drawable) drawable).setBounds(0, 0, realWidth, realHeight);
下面是另一套实现思路代码:
DraweeTextView.java
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Spanned;
import android.util.AttributeSet;
import android.widget.TextView;
public class DraweeTextView extends TextView {
public DraweeTextView(Context context) {
super(context);
}
public DraweeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DraweeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public DraweeTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private boolean mHasDraweeInText;
// detect drawee-spans has been attached or not
private boolean mIsSpanAttached;
@Override
public void setText(CharSequence text, BufferType type) {
boolean wasSpanAttached = mIsSpanAttached;
if (mHasDraweeInText && wasSpanAttached) {
onDetach(); // detach all old images
mHasDraweeInText = false;
}
if (text instanceof Spanned) {
// find DraweeSpan in text
DraweeSpan[] spans = ((Spanned) text).getSpans(0, text.length(), DraweeSpan.class);
mHasDraweeInText = spans.length > 0;
}
super.setText(text, type);
if (mHasDraweeInText && wasSpanAttached) {
onAttach(); // reattach drawee spans
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
onAttach();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
onDetach();
}
@Override
public void onStartTemporaryDetach() {
super.onStartTemporaryDetach();
onDetach();
}
@Override
public void onFinishTemporaryDetach() {
super.onFinishTemporaryDetach();
onAttach();
}
@Override
public void invalidateDrawable(Drawable dr) {
if (mHasDraweeInText) {
/* invalidate the whole view in this case because it's very
* hard to know what the bounds of drawables actually is.
*/
invalidate();
} else {
super.invalidateDrawable(dr);
}
}
/**
* Attach DraweeSpans in text
*/
final void onAttach() {
DraweeSpan[] images = getImages();
for (DraweeSpan image : images) {
image.onAttach(this);
}
mIsSpanAttached = true;
}
private DraweeSpan[] getImages() {
try {
if (mHasDraweeInText && length() > 0) {
return ((Spanned) getText()).getSpans(0, length(), DraweeSpan.class);
}
} catch (Exception e) {
}
return new DraweeSpan[0]; //TODO: pool empty typed array
}
/**
* Detach all of the DraweeSpans in text
*/
final void onDetach() {
DraweeSpan[] images = getImages();
for (DraweeSpan image : images) {
Drawable drawable = image.getDrawable();
// reset callback first
if (drawable != null) {
unscheduleDrawable(drawable);
}
image.onDetach();
}
mIsSpanAttached = false;
}
}
DraweeSpan.java
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.text.style.DynamicDrawableSpan;
import android.view.View;
import com.xx.xx.painter.image.ImageLoadRequestListener;
import com.xx.xx.painter.image.Painter;
import com.xx.xx.painter.image.request.RequestParams;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class DraweeSpan extends DynamicDrawableSpan implements DeferredReleaser.Releasable {
private final DeferredReleaser mDeferredReleaser;
private final ForwardingDrawable mActualDrawable;
private Drawable mDrawable;
private Drawable mPlaceHolder;
private View mAttachedView;
private String mImageUri;
private boolean mIsAttached;
private Rect mMargin = new Rect();
private Point mLayout = new Point();
/**
* 顶部对齐
*/
public static final int ALIGN_TOP = 3;
/**
* 垂直居中
*/
public static final int ALIGN_CENTER = 4;
@IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_TOP, ALIGN_CENTER})
@Retention(RetentionPolicy.SOURCE)
public @interface Alignment {
}
public DraweeSpan(String uri) {
this(uri, createEmptyDrawable());
}
public DraweeSpan(String uri, int width, int height) {
this(uri, createEmptyDrawable(width, height));
}
public DraweeSpan(String uri, Drawable placeHolder) {
this(uri, ALIGN_CENTER, placeHolder);
}
public DraweeSpan(String uri, @Alignment int verticalAlignment, Drawable placeHolder) {
super(verticalAlignment);
mImageUri = uri;
mDeferredReleaser = DeferredReleaser.getInstance();
mPlaceHolder = placeHolder;
// create forwarding drawable with placeholder
mActualDrawable = new ForwardingDrawable(mPlaceHolder);
Rect bounds = mPlaceHolder.getBounds();
if (bounds.right == 0 || bounds.bottom == 0) {
mActualDrawable.setBounds(0, 0, mPlaceHolder.getIntrinsicWidth(), mPlaceHolder.getIntrinsicHeight());
} else {
mActualDrawable.setBounds(bounds);
}
}
private static Drawable createEmptyDrawable(int width, int height) {
ColorDrawable d = new ColorDrawable(Color.TRANSPARENT);
d.setBounds(0, 0, width, height);
return d;
}
private static Drawable createEmptyDrawable() {
ColorDrawable d = new ColorDrawable(Color.TRANSPARENT);
d.setBounds(0, 0, 100, 100);
return d;
}
protected void layout() {
mActualDrawable.setBounds(0, 0, mLayout.x, mLayout.y);
}
@Override
public Drawable getDrawable() {
return mActualDrawable;
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
Drawable d = getDrawable();
Rect rect = d.getBounds();
try {
if (fm != null) {
Paint.FontMetrics fmPaint = paint.getFontMetrics();
// 顶部 leading
float topLeading = fmPaint.top - fmPaint.ascent;
// 底部 leading
float bottomLeading = fmPaint.bottom - fmPaint.descent;
// drawable 的高度
int drHeight = rect.height();
switch (mVerticalAlignment) {
case ALIGN_CENTER: { // drawable 的中间与 行中间对齐
// 当前行 的高度
float fontHeight = fmPaint.descent - fmPaint.ascent;
// 整行的 y方向上的中间 y 坐标
float center = fmPaint.descent - fontHeight / 2;
// 算出 ascent 和 descent
float ascent = center - drHeight / 2;
float descent = center + drHeight / 2;
fm.ascent = (int) ascent;
fm.top = (int) (ascent + topLeading);
fm.descent = (int) descent;
fm.bottom = (int) (descent + bottomLeading);
break;
}
case ALIGN_BASELINE: { // drawable 的底部与 baseline 对齐
// 所以 ascent 的值就是 负的 drawable 的高度
float ascent = -drHeight;
fm.ascent = -drHeight;
fm.top = (int) (ascent + topLeading);
break;
}
case ALIGN_TOP: { // drawable 的顶部与 行的顶部 对齐
// 算出 descent
float descent = drHeight + fmPaint.ascent;
fm.descent = (int) descent;
fm.bottom = (int) (descent + bottomLeading);
break;
}
case ALIGN_BOTTOM: // drawable 的底部与 行的底部 对齐
default: {
// 算出 ascent
float ascent = fmPaint.descent - drHeight;
fm.ascent = (int) ascent;
fm.top = (int) (ascent + topLeading);
}
}
}
} catch (Exception e) {
}
return rect.right + mMargin.left + mMargin.right;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
try{
Drawable drawable = getDrawable();
Rect rect = drawable.getBounds();
float transY;
switch (mVerticalAlignment) {
case ALIGN_BASELINE:
transY = y - rect.height();
break;
case ALIGN_CENTER:
transY = ((bottom - top) - drawable.getBounds().bottom) / 2 + top;
break;
case ALIGN_TOP:
transY = top;
break;
case ALIGN_BOTTOM:
default:
transY = bottom - rect.height();
}
canvas.save();
canvas.translate(x + mMargin.left, transY);
drawable.draw(canvas);
canvas.restore();
}catch (Exception e){
}
}
public void setImageWithIntrinsicBounds(Drawable drawable) {
if (mDrawable != drawable) {
mActualDrawable.setDrawable(drawable);
mDrawable = drawable;
}
}
public void reset() {
mActualDrawable.setDrawable(mPlaceHolder);
}
/**
* set bounds
*/
public void setSize(int width, int height) {
mActualDrawable.setBounds(0, 0, width, height);
}
public void onAttach(@NonNull View view) {
mIsAttached = true;
if (mAttachedView != view) {
mActualDrawable.setCallback(null);
if (mAttachedView != null) {
throw new IllegalStateException("has been attached to view:" + mAttachedView);
}
mAttachedView = view;
mActualDrawable.setCallback(mAttachedView);
}
mDeferredReleaser.cancelDeferredRelease(this);
submitRequest();
}
private void submitRequest() {
ImageLoadRequestListener imageLoadRequestListener = new DraweeSpanImageLoadRequestListener(mAttachedView.getContext());
Painter.getInstance().loadPhoto(imageLoadRequestListener, RequestParams.get().url(getImageUri()).asBitmap(true));
}
public class DraweeSpanImageLoadRequestListener extends ImageLoadRequestListener<Bitmap> {
private final Context context;
public DraweeSpanImageLoadRequestListener(Context context) {
this.context = context;
}
@Override
public void setResource(Bitmap resource) {
Drawable drawable;
try {
drawable = new BitmapDrawable(context.getResources(), resource);
setImageWithIntrinsicBounds(drawable);
} catch (Exception exception) {
}
}
@Override
public void onSucess() {
}
@Override
public Context getContext() {
return context;
}
}
@NonNull
protected String getImageUri() {
return mImageUri;
}
protected String getId() {
return String.valueOf(getImageUri().hashCode());
}
public void onDetach() {
if (!mIsAttached)
return;
mActualDrawable.setCallback(null);
mAttachedView = null;
reset();
mDeferredReleaser.scheduleDeferredRelease(this);
}
@Override
public void release() {
mIsAttached = false;
mAttachedView = null;
mDrawable = null;
}
public static class Builder {
String uri;
int width = 100;
int height = 100;
int verticalAlignment = ALIGN_CENTER;
Drawable placeholder;
Rect margin = new Rect();
public Builder(String uri) {
this(uri, false);
}
/**
* Construct drawee span builder.
*
* @param uri image uri.
* @param alignBaseline true to set {@link #ALIGN_BASELINE}, otherwise {@link #ALIGN_BOTTOM} .
*/
public Builder(String uri, boolean alignBaseline) {
this.uri = uri;
if (uri == null) {
throw new NullPointerException("Attempt to create a DraweeSpan with null uri string!");
}
if (alignBaseline) {
this.verticalAlignment = ALIGN_CENTER;
}
}
/**
* @param width width of this span, px
* @param height height of this span, px
*/
public Builder setLayout(int width, int height) {
this.width = width;
this.height = height;
return this;
}
/**
* You can set margin in left, right and top in px. Bottom is in baseline.
*/
public Builder setMargin(int margin) {
this.margin.set(margin, margin, margin, 0);
return this;
}
/**
* You can set margin in left, right and top in px. Bottom is in baseline.
*/
public Builder setMargin(int left, int top, int right) {
this.margin.set(left, top, right, 0);
return this;
}
/**
* @param placeholder The drawable shows on loading image {@code uri}
*/
public Builder setPlaceHolderImage(Drawable placeholder) {
this.placeholder = placeholder;
return this;
}
public DraweeSpan build() {
if (placeholder == null) {
placeholder = new ColorDrawable(Color.TRANSPARENT);
placeholder.setBounds(0, 0, width, height);
}
DraweeSpan span = new DraweeSpan(uri, verticalAlignment, placeholder);
span.mLayout.set(width, height);
span.mMargin.set(margin.left, margin.top, margin.right, 0);
span.layout();
return span;
}
}
}
DeferredReleaser.java
import android.os.Handler;
import android.os.Looper;
import java.util.HashSet;
import java.util.Set;
/**
* Component that defers {@code release} until after the main Looper has completed its current
* message. Although we would like for defer {@code release} to happen immediately after the current
* message is done, this is not guaranteed as there might be other messages after the current one,
* but before the deferred one, pending in the Looper's queue.
*
* onDetach / onAttach events are used for releasing / acquiring resources. However, sometimes we
* get an onDetach event followed by an onAttach event within the same loop. In order to avoid
* overaggressive resource releasing / acquiring, we defer releasing. If onAttach happens within
* the same loop, we will simply cancel corresponding deferred release, avoiding an unnecessary
* resource release / acquire cycle. If onAttach doesn't happen before the deferred message gets
* executed, the resources will be released.
*
* This class is not thread-safe and should only be used from the main thread (UI thread).
*/
public class DeferredReleaser {
private static DeferredReleaser sInstance = null;
public static synchronized DeferredReleaser getInstance() {
if (sInstance == null) {
sInstance = new DeferredReleaser();
}
return sInstance;
}
public interface Releasable {
public void release();
}
private final Set<Releasable> mPendingReleasables;
private final Handler mUiHandler;
public DeferredReleaser() {
mPendingReleasables = new HashSet<Releasable>();
mUiHandler = new Handler(Looper.getMainLooper());
}
/*
* Walks through the set of pending releasables, and calls release on them.
* Resets the pending list to an empty list when done.
*/
private final Runnable releaseRunnable = new Runnable() {
@Override
public void run() {
ensureOnUiThread();
for (Releasable releasable : mPendingReleasables) {
releasable.release();
}
mPendingReleasables.clear();
}
};
/**
* Schedules deferred release.
*
* The object will be released after the current Looper's loop,
* unless {@code cancelDeferredRelease} is called before then.
* @param releasable Object to release.
*/
public void scheduleDeferredRelease(Releasable releasable) {
ensureOnUiThread();
if (!mPendingReleasables.add(releasable)) {
return;
}
// Posting to the UI queue is an O(n) operation, so we only do it once.
// The one runnable does all the releases.
if (mPendingReleasables.size() == 1) {
mUiHandler.post(releaseRunnable);
}
}
/**
* Cancels a pending release for this object.
* @param releasable Object to cancel release of.
*/
public void cancelDeferredRelease(Releasable releasable) {
ensureOnUiThread();
mPendingReleasables.remove(releasable);
}
private static void ensureOnUiThread() {
Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread());
}
}
ForwardingDrawable.java
import android.annotation.TargetApi;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Build;
/**
* A forwarding drawable class - the goal is to forward (delegate) drawable functionality to an
* inner drawable instance. ForwardingDrawable intercepts the public (and protected) methods of
* {@link Drawable}, maintains local state if needed.
*
* Design note: It would have been very helpful to re-use Android library classes
* like DrawableContainer, LevelListDrawable etc. DrawableContainer is not directly subclassable,
* and the others don't allow changing the member drawables.
*/
public class ForwardingDrawable extends Drawable
implements Drawable.Callback, TransformCallback, TransformAwareDrawable, DrawableParent {
/** The current drawable to be drawn by this drawable when drawing is needed */
private Drawable mCurrentDelegate;
private final DrawableProperties mDrawableProperties = new DrawableProperties();
protected TransformCallback mTransformCallback;
/**
* Matrix used to store temporary transform. Drawables should be accessed on UI thread only, and
* this matrix is used only as a temporary variable so it's safe to be static.
*/
private static final Matrix sTempTransform = new Matrix();
/**
* Constructs a new forwarding drawable.
* @param drawable drawable that this forwarding drawable will forward to
*/
public ForwardingDrawable(Drawable drawable) {
mCurrentDelegate = drawable;
DrawableUtils.setCallbacks(mCurrentDelegate, this, this);
}
/**
* Sets a new drawable to be the delegate, and returns the old one (or null).
*
* This method will cause the drawable to be invalidated.
* @param newDelegate
* @return the previous delegate
*/
public Drawable setCurrent(Drawable newDelegate) {
Drawable previousDelegate = setCurrentWithoutInvalidate(newDelegate);
invalidateSelf();
return previousDelegate;
}
/**
* As {@code setCurrent}, but without invalidating a drawable. Subclasses are responsible to call
* {@code invalidateSelf} on their own.
* @param newDelegate
* @return the previous delegate
*/
protected Drawable setCurrentWithoutInvalidate(Drawable newDelegate) {
Drawable previousDelegate = mCurrentDelegate;
DrawableUtils.setCallbacks(previousDelegate, null, null);
DrawableUtils.setCallbacks(newDelegate, null, null);
DrawableUtils.setDrawableProperties(newDelegate, mDrawableProperties);
DrawableUtils.copyProperties(newDelegate, this);
DrawableUtils.setCallbacks(newDelegate, this, this);
mCurrentDelegate = newDelegate;
return previousDelegate;
}
@Override
public int getOpacity() {
return mCurrentDelegate.getOpacity();
}
@Override
public void setAlpha(int alpha) {
mDrawableProperties.setAlpha(alpha);
mCurrentDelegate.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mDrawableProperties.setColorFilter(colorFilter);
mCurrentDelegate.setColorFilter(colorFilter);
}
@Override
public void setDither(boolean dither) {
mDrawableProperties.setDither(dither);
mCurrentDelegate.setDither(dither);
}
@Override
public void setFilterBitmap(boolean filterBitmap) {
mDrawableProperties.setFilterBitmap(filterBitmap);
mCurrentDelegate.setFilterBitmap(filterBitmap);
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
super.setVisible(visible, restart);
return mCurrentDelegate.setVisible(visible, restart);
}
@Override
protected void onBoundsChange(Rect bounds) {
mCurrentDelegate.setBounds(bounds);
}
@Override
public boolean isStateful() {
return mCurrentDelegate.isStateful();
}
@Override
protected boolean onStateChange(int[] state) {
return mCurrentDelegate.setState(state);
}
@Override
protected boolean onLevelChange(int level) {
return mCurrentDelegate.setLevel(level);
}
@Override
public void draw(Canvas canvas) {
mCurrentDelegate.draw(canvas);
}
@Override
public int getIntrinsicWidth() {
return mCurrentDelegate.getIntrinsicWidth();
}
@Override
public int getIntrinsicHeight() {
return mCurrentDelegate.getIntrinsicHeight();
}
@Override
public boolean getPadding(Rect padding) {
return mCurrentDelegate.getPadding(padding);
}
@Override
public Drawable mutate() {
mCurrentDelegate.mutate();
return this;
}
@Override
public Drawable getCurrent() {
return mCurrentDelegate;
}
// DrawableParent methods
@Override
public Drawable setDrawable(Drawable newDrawable) {
return setCurrent(newDrawable);
}
@Override
public Drawable getDrawable() {
return getCurrent();
}
// Drawable.Callback methods
@Override
public void invalidateDrawable(Drawable who) {
invalidateSelf();
}
@Override
public void scheduleDrawable(Drawable who, Runnable what, long when) {
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(Drawable who, Runnable what) {
unscheduleSelf(what);
}
// TransformAwareDrawable methods
@Override
public void setTransformCallback(TransformCallback transformCallback) {
mTransformCallback = transformCallback;
}
// TransformationCallback methods
protected void getParentTransform(Matrix transform) {
if (mTransformCallback != null) {
mTransformCallback.getTransform(transform);
} else {
transform.reset();
}
}
@Override
public void getTransform(Matrix transform) {
getParentTransform(transform);
}
@Override
public void getRootBounds(RectF bounds) {
if (mTransformCallback != null) {
mTransformCallback.getRootBounds(bounds);
} else {
bounds.set(getBounds());
}
}
/**
* Gets the transformed bounds of this drawable.
* Note: bounds are not cropped (otherwise they would likely be the same as drawable's bounds).
* @param outBounds rect to fill with bounds
*/
public void getTransformedBounds(RectF outBounds) {
getParentTransform(sTempTransform);
// IMPORTANT: {@code getBounds} should be called after {@code getParentTransform},
// because the parent may have to change our bounds.
outBounds.set(getBounds());
sTempTransform.mapRect(outBounds);
}
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void setHotspot(float x, float y) {
mCurrentDelegate.setHotspot(x, y);
}
}
TransformCallback.java
import android.graphics.Matrix;
import android.graphics.RectF;
/**
* Callback that is used to pass any transformation matrix and the root bounds from a parent
* drawable to its child.
*/
public interface TransformCallback {
/**
* Called when the drawable needs to get all matrices applied to it.
*
* @param transform Matrix that is applied to the drawable by the parent drawables.
*/
void getTransform(Matrix transform);
/**
* Called when the drawable needs to get its root bounds.
*
* @param bounds The root bounds of the drawable.
*/
void getRootBounds(RectF bounds);
}
TransformAwareDrawable.java
/**
* Interface that enables setting a transform callback.
*/
public interface TransformAwareDrawable {
/**
* Sets a transform callback.
*
* @param transformCallback the transform callback to be set
*/
void setTransformCallback(TransformCallback transformCallback);
}
Preconditions.java
public final class Preconditions {
private Preconditions() {}
/**
* Ensures the truth of an expression involving one or more parameters to the calling method.
*
* @param expression a boolean expression
* @throws IllegalArgumentException if {@code expression} is false
*/
public static void checkArgument(boolean expression) {
if (!expression) {
throw new IllegalArgumentException();
}
}
/**
* Ensures the truth of an expression involving one or more parameters to the calling method.
*
* @param expression a boolean expression
* @param errorMessage the exception message to use if the check fails; will be converted to a
* string using {@link String#valueOf(Object)}
* @throws IllegalArgumentException if {@code expression} is false
*/
public static void checkArgument(boolean expression, @Nullable Object errorMessage) {
if (!expression) {
throw new IllegalArgumentException(String.valueOf(errorMessage));
}
}
/**
* Ensures the truth of an expression involving one or more parameters to the calling method.
*
* @param expression a boolean expression
* @param errorMessageTemplate a template for the exception message should the check fail. The
* message is formed by replacing each {@code %s} placeholder in the template with an
* argument. These are matched by position - the first {@code %s} gets {@code
* errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message
* in square braces. Unmatched placeholders will be left as-is.
* @param errorMessageArgs the arguments to be substituted into the message template. Arguments
* are converted to strings using {@link String#valueOf(Object)}.
* @throws IllegalArgumentException if {@code expression} is false
* @throws NullPointerException if the check fails and either {@code errorMessageTemplate} or
* {@code errorMessageArgs} is null (don't let this happen)
*/
public static void checkArgument(boolean expression,
@Nullable String errorMessageTemplate,
@Nullable Object... errorMessageArgs) {
if (!expression) {
throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageArgs));
}
}
/**
* Ensures the truth of an expression involving the state of the calling instance, but not
* involving any parameters to the calling method.
*
* @param expression a boolean expression
* @throws IllegalStateException if {@code expression} is false
*/
public static void checkState(boolean expression) {
if (!expression) {
throw new IllegalStateException();
}
}
/**
* Ensures the truth of an expression involving the state of the calling instance, but not
* involving any parameters to the calling method.
*
* @param expression a boolean expression
* @param errorMessage the exception message to use if the check fails; will be converted to a
* string using {@link String#valueOf(Object)}
* @throws IllegalStateException if {@code expression} is false
*/
public static void checkState(boolean expression, @Nullable Object errorMessage) {
if (!expression) {
throw new IllegalStateException(String.valueOf(errorMessage));
}
}
/**
* Ensures the truth of an expression involving the state of the calling instance, but not
* involving any parameters to the calling method.
*
* @param expression a boolean expression
* @param errorMessageTemplate a template for the exception message should the check fail. The
* message is formed by replacing each {@code %s} placeholder in the template with an
* argument. These are matched by position - the first {@code %s} gets {@code
* errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message
* in square braces. Unmatched placeholders will be left as-is.
* @param errorMessageArgs the arguments to be substituted into the message template. Arguments
* are converted to strings using {@link String#valueOf(Object)}.
* @throws IllegalStateException if {@code expression} is false
* @throws NullPointerException if the check fails and either {@code errorMessageTemplate} or
* {@code errorMessageArgs} is null (don't let this happen)
*/
public static void checkState(boolean expression,
@Nullable String errorMessageTemplate,
@Nullable Object... errorMessageArgs) {
if (!expression) {
throw new IllegalStateException(format(errorMessageTemplate, errorMessageArgs));
}
}
/**
* Ensures that an object reference passed as a parameter to the calling method is not null.
*
* @param reference an object reference
* @return the non-null reference that was validated
* @throws NullPointerException if {@code reference} is null
*/
public static <T> T checkNotNull(T reference) {
if (reference == null) {
throw new NullPointerException();
}
return reference;
}
/**
* Ensures that an object reference passed as a parameter to the calling method is not null.
*
* @param reference an object reference
* @param errorMessage the exception message to use if the check fails; will be converted to a
* string using {@link String#valueOf(Object)}
* @return the non-null reference that was validated
* @throws NullPointerException if {@code reference} is null
*/
public static <T> T checkNotNull(T reference, @Nullable Object errorMessage) {
if (reference == null) {
throw new NullPointerException(String.valueOf(errorMessage));
}
return reference;
}
/**
* Ensures that an object reference passed as a parameter to the calling method is not null.
*
* @param reference an object reference
* @param errorMessageTemplate a template for the exception message should the check fail. The
* message is formed by replacing each {@code %s} placeholder in the template with an
* argument. These are matched by position - the first {@code %s} gets {@code
* errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message
* in square braces. Unmatched placeholders will be left as-is.
* @param errorMessageArgs the arguments to be substituted into the message template. Arguments
* are converted to strings using {@link String#valueOf(Object)}.
* @return the non-null reference that was validated
* @throws NullPointerException if {@code reference} is null
*/
public static <T> T checkNotNull(T reference,
@Nullable String errorMessageTemplate,
@Nullable Object... errorMessageArgs) {
if (reference == null) {
// If either of these parameters is null, the right thing happens anyway
throw new NullPointerException(format(errorMessageTemplate, errorMessageArgs));
}
return reference;
}
/*
* All recent hotspots (as of 2009) *really* like to have the natural code
*
* if (guardExpression) {
* throw new BadException(messageExpression);
* }
*
* refactored so that messageExpression is moved to a separate String-returning method.
*
* if (guardExpression) {
* throw new BadException(badMsg(...));
* }
*
* The alternative natural refactorings into void or Exception-returning methods are much slower.
* This is a big deal - we're talking factors of 2-8 in microbenchmarks, not just 10-20%. (This
* is a hotspot optimizer bug, which should be fixed, but that's a separate, big project).
*
* The coding pattern above is heavily used in java.util, e.g. in ArrayList. There is a
* RangeCheckMicroBenchmark in the JDK that was used to test this.
*
* But the methods in this class want to throw different exceptions, depending on the args, so it
* appears that this pattern is not directly applicable. But we can use the ridiculous, devious
* trick of throwing an exception in the middle of the construction of another exception. Hotspot
* is fine with that.
*/
/**
* Ensures that {@code index} specifies a valid element in an array, list or string of size
* {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive.
*
* @param index a user-supplied index identifying an element of an array, list or string
* @param size the size of that array, list or string
* @return the value of {@code index}
* @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size}
* @throws IllegalArgumentException if {@code size} is negative
*/
public static int checkElementIndex(int index, int size) {
return checkElementIndex(index, size, "index");
}
/**
* Ensures that {@code index} specifies a valid element in an array, list or string of size
* {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive.
*
* @param index a user-supplied index identifying an element of an array, list or string
* @param size the size of that array, list or string
* @param desc the text to use to describe this index in an error message
* @return the value of {@code index}
* @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size}
* @throws IllegalArgumentException if {@code size} is negative
*/
public static int checkElementIndex(
int index, int size, @Nullable String desc) {
// Carefully optimized for execution by hotspot (explanatory comment above)
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException(badElementIndex(index, size, desc));
}
return index;
}
private static String badElementIndex(int index, int size, @Nullable String desc) {
if (index < 0) {
return format("%s (%s) must not be negative", desc, index);
} else if (size < 0) {
throw new IllegalArgumentException("negative size: " + size);
} else { // index >= size
return format("%s (%s) must be less than size (%s)", desc, index, size);
}
}
/**
* Ensures that {@code index} specifies a valid position in an array, list or string of
* size {@code size}. A position index may range from zero to {@code size}, inclusive.
*
* @param index a user-supplied index identifying a position in an array, list or string
* @param size the size of that array, list or string
* @return the value of {@code index}
* @throws IndexOutOfBoundsException if {@code index} is negative or is greater than {@code size}
* @throws IllegalArgumentException if {@code size} is negative
*/
public static int checkPositionIndex(int index, int size) {
return checkPositionIndex(index, size, "index");
}
/**
* Ensures that {@code index} specifies a valid position in an array, list or string of
* size {@code size}. A position index may range from zero to {@code size}, inclusive.
*
* @param index a user-supplied index identifying a position in an array, list or string
* @param size the size of that array, list or string
* @param desc the text to use to describe this index in an error message
* @return the value of {@code index}
* @throws IndexOutOfBoundsException if {@code index} is negative or is greater than {@code size}
* @throws IllegalArgumentException if {@code size} is negative
*/
public static int checkPositionIndex(int index, int size, @Nullable String desc) {
// Carefully optimized for execution by hotspot (explanatory comment above)
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException(badPositionIndex(index, size, desc));
}
return index;
}
private static String badPositionIndex(int index, int size, @Nullable String desc) {
if (index < 0) {
return format("%s (%s) must not be negative", desc, index);
} else if (size < 0) {
throw new IllegalArgumentException("negative size: " + size);
} else { // index > size
return format("%s (%s) must not be greater than size (%s)", desc, index, size);
}
}
/**
* Ensures that {@code start} and {@code end} specify a valid positions in an array, list
* or string of size {@code size}, and are in order. A position index may range from zero to
* {@code size}, inclusive.
*
* @param start a user-supplied index identifying a starting position in an array, list or string
* @param end a user-supplied index identifying a ending position in an array, list or string
* @param size the size of that array, list or string
* @throws IndexOutOfBoundsException if either index is negative or is greater than {@code size},
* or if {@code end} is less than {@code start}
* @throws IllegalArgumentException if {@code size} is negative
*/
public static void checkPositionIndexes(int start, int end, int size) {
// Carefully optimized for execution by hotspot (explanatory comment above)
if (start < 0 || end < start || end > size) {
throw new IndexOutOfBoundsException(badPositionIndexes(start, end, size));
}
}
private static String badPositionIndexes(int start, int end, int size) {
if (start < 0 || start > size) {
return badPositionIndex(start, size, "start index");
}
if (end < 0 || end > size) {
return badPositionIndex(end, size, "end index");
}
// end < start
return format("end index (%s) must not be less than start index (%s)", end, start);
}
/**
* Substitutes each {@code %s} in {@code template} with an argument. These are matched by
* position: the first {@code %s} gets {@code args[0]}, etc. If there are more arguments than
* placeholders, the unmatched arguments will be appended to the end of the formatted message in
* square braces.
*
* @param template a non-null string containing 0 or more {@code %s} placeholders.
* @param args the arguments to be substituted into the message template. Arguments are converted
* to strings using {@link String#valueOf(Object)}. Arguments can be null.
*/
// Note that this is somewhat-improperly used from Verify.java as well.
static String format(@Nullable String template, @Nullable Object... args) {
template = String.valueOf(template); // null -> "null"
// start substituting the arguments into the '%s' placeholders
StringBuilder builder = new StringBuilder(template.length() + 16 * args.length);
int templateStart = 0;
int i = 0;
while (i < args.length) {
int placeholderStart = template.indexOf("%s", templateStart);
if (placeholderStart == -1) {
break;
}
builder.append(template.substring(templateStart, placeholderStart));
builder.append(args[i++]);
templateStart = placeholderStart + 2;
}
builder.append(template.substring(templateStart));
// if we run out of placeholders, append the extra args in square braces
if (i < args.length) {
builder.append(" [");
builder.append(args[i++]);
while (i < args.length) {
builder.append(", ");
builder.append(args[i++]);
}
builder.append(']');
}
return builder.toString();
}
}
DrawableUtils.java
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
/**
* Helper class containing functionality commonly used by drawables.
*/
public class DrawableUtils {
/**
* Copies various properties from one drawable to the other.
* @param to drawable to copy properties to
* @param from drawable to copy properties from
*/
public static void copyProperties(Drawable to, Drawable from) {
if (from == null || to == null || to == from) {
return;
}
to.setBounds(from.getBounds());
to.setChangingConfigurations(from.getChangingConfigurations());
to.setLevel(from.getLevel());
to.setVisible(from.isVisible(), /* restart */ false);
to.setState(from.getState());
}
/**
* Sets various paint properties on the drawable
* @param drawable Drawable on which to set the properties
* @param properties wrapper around mValue values to set on the drawable
*/
public static void setDrawableProperties(Drawable drawable, DrawableProperties properties) {
if (drawable == null || properties == null) {
return;
}
properties.applyTo(drawable);
}
/**
* Sets callback to the drawable.
* @param drawable drawable to set callbacks to
* @param callback standard Android Drawable.Callback
* @param transformCallback TransformCallback used by TransformAwareDrawables
*/
public static void setCallbacks(
Drawable drawable,
@Nullable Drawable.Callback callback,
@Nullable TransformCallback transformCallback) {
if (drawable != null) {
drawable.setCallback(callback);
if (drawable instanceof TransformAwareDrawable) {
((TransformAwareDrawable) drawable).setTransformCallback(transformCallback);
}
}
}
/**
* Multiplies the color with the given alpha.
* @param color color to be multiplied
* @param alpha value between 0 and 255
* @return multiplied color
*/
public static int multiplyColorAlpha(int color, int alpha) {
if (alpha == 255) {
return color;
}
if (alpha == 0) {
return color & 0x00FFFFFF;
}
alpha = alpha + (alpha >> 7); // make it 0..256
int colorAlpha = color >>> 24;
int multipliedAlpha = colorAlpha * alpha >> 8;
return (multipliedAlpha << 24) | (color & 0x00FFFFFF);
}
/**
* Gets the opacity from a color. Inspired by Android ColorDrawable.
* @param color
* @return opacity expressed by one of PixelFormat constants
*/
public static int getOpacityFromColor(int color) {
int colorAlpha = color >>> 24;
if (colorAlpha == 255) {
return PixelFormat.OPAQUE;
} else if (colorAlpha == 0) {
return PixelFormat.TRANSPARENT;
} else {
return PixelFormat.TRANSLUCENT;
}
}
}
DrawableProperties.java
public class DrawableProperties {
private static final int UNSET = -1;
private int mAlpha = UNSET;
private boolean mIsSetColorFilter = false;
private ColorFilter mColorFilter = null;
private int mDither = UNSET;
private int mFilterBitmap = UNSET;
public void setAlpha(int alpha) {
mAlpha = alpha;
}
public void setColorFilter(ColorFilter colorFilter) {
mColorFilter = colorFilter;
mIsSetColorFilter = true;
}
public void setDither(boolean dither) {
mDither = dither ? 1 : 0;
}
public void setFilterBitmap(boolean filterBitmap) {
mFilterBitmap = filterBitmap ? 1 : 0;
}
public void applyTo(Drawable drawable) {
if (drawable == null) {
return;
}
if (mAlpha != UNSET) {
drawable.setAlpha(mAlpha);
}
if (mIsSetColorFilter) {
drawable.setColorFilter(mColorFilter);
}
if (mDither != UNSET) {
drawable.setDither(mDither != 0);
}
if (mFilterBitmap != UNSET) {
drawable.setFilterBitmap(mFilterBitmap != 0);
}
}
}
DrawableParent.java
public interface DrawableParent {
/**
* Sets the new child drawable.
* @param newDrawable a new child drawable to set
* @return the old child drawable
*/
Drawable setDrawable(Drawable newDrawable);
/**
* Gets the child drawable.
* @return the current child drawable
*/
Drawable getDrawable();
}