Android 富文本 SpannableString 支持网络图ImageSpan

富文本 SpannableString 支持网络图

实现思路: 在文本前加空格占位,使用默认图填充,下载网络图,下载完成时替换默认图。
CenterVerticalImageSpan 实现图片在文字中居中效果。
记得网络图回来后,还要调用 textView.setText(spannableString, TextView.BufferType.SPANNABLE);

效果图:
Android 富文本 SpannableString 支持网络图ImageSpan_第1张图片

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();

}

你可能感兴趣的:(Android,UI,开发总结,工作总结,android,富文本,ImageSpan)