/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.drawee.span; import android.content.Context; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.text.SpannableStringBuilder; import android.view.View; import com.facebook.common.internal.Preconditions; import com.facebook.common.internal.VisibleForTesting; import com.facebook.common.lifecycle.AttachDetachListener; import com.facebook.drawee.controller.AbstractDraweeController; import com.facebook.drawee.controller.BaseControllerListener; import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.interfaces.DraweeHierarchy; import com.facebook.drawee.view.DraweeHolder; import com.facebook.imagepipeline.image.ImageInfo; import com.facebook.widget.text.span.BetterImageSpan; import java.util.HashSet; import java.util.Set; /** * DraweeSpanStringBuilder that can be used to add {@link DraweeSpan}s to strings. * * <p>The containing view must also call {@link #onDetachFromView(View)} ()} from its {@link * View#onStartTemporaryDetach()} and {@link View#onDetachedFromWindow()} methods. Similarly, it * must call {@link #onAttachToView(View)} from its {@link View#onFinishTemporaryDetach()} and * {@link View#onAttachedToWindow()} methods. * * <p>If you attach the same DraweeSpanStringBuilder to different views, only the most recent view * will be updated correctly since you can only bind the same builder to 1 view at a time. Older * views will be automatically unbound. * * {@see DraweeHolder} */ public class DraweeSpanStringBuilder extends SpannableStringBuilder implements AttachDetachListener { public interface DraweeSpanChangedListener { public void onDraweeSpanChanged(DraweeSpanStringBuilder draweeSpanStringBuilder); } public static final int UNSET_SIZE = -1; private final Set<DraweeSpan> mDraweeSpans = new HashSet<>(); private final DrawableCallback mDrawableCallback = new DrawableCallback(); private View mBoundView; private Drawable mBoundDrawable; private DraweeSpanChangedListener mDraweeSpanChangedListener; public DraweeSpanStringBuilder() { super(); } public DraweeSpanStringBuilder(CharSequence text) { super(text); } public DraweeSpanStringBuilder(CharSequence text, int start, int end) { super(text, start, end); } public void setImageSpan( DraweeHolder draweeHolder, int index, final int drawableWidthPx, final int drawableHeightPx, boolean enableResizing, @BetterImageSpan.BetterImageSpanAlignment int verticalAlignment) { setImageSpan( draweeHolder, index, index, drawableWidthPx, drawableHeightPx, enableResizing, verticalAlignment); } public void setImageSpan( DraweeHolder draweeHolder, int startIndex, int endIndex, final int drawableWidthPx, final int drawableHeightPx, boolean enableResizing, @BetterImageSpan.BetterImageSpanAlignment int verticalAlignment) { if (endIndex >= length()) { // Unfortunately, some callers use this wrong. The original implementation also swallows // an exception if this happens (e.g. if you tap on a video that has a minutiae as well. // Example: Text = "ABC", insert image at position 18. return; } Drawable topLevelDrawable = draweeHolder.getTopLevelDrawable(); if (topLevelDrawable != null) { if (topLevelDrawable.getBounds().isEmpty()) { topLevelDrawable.setBounds(0, 0, drawableWidthPx, drawableHeightPx); } topLevelDrawable.setCallback(mDrawableCallback); } DraweeSpan draweeSpan = new DraweeSpan(draweeHolder, verticalAlignment); final DraweeController controller = draweeHolder.getController(); if (controller instanceof AbstractDraweeController) { ((AbstractDraweeController) controller).addControllerListener( new DrawableChangedListener(draweeSpan, enableResizing, drawableHeightPx)); } mDraweeSpans.add(draweeSpan); setSpan(draweeSpan, startIndex, endIndex + 1, SPAN_EXCLUSIVE_EXCLUSIVE); } public void setImageSpan( Context context, DraweeHierarchy draweeHierarchy, DraweeController draweeController, int index, final int drawableWidthPx, final int drawableHeightPx, boolean enableResizing, @BetterImageSpan.BetterImageSpanAlignment int verticalAlignment) { setImageSpan( context, draweeHierarchy, draweeController, index, index, drawableWidthPx, drawableHeightPx, enableResizing, verticalAlignment); } public void setImageSpan( Context context, DraweeHierarchy draweeHierarchy, DraweeController draweeController, int startIndex, int endIndex, final int drawableWidthPx, final int drawableHeightPx, boolean enableResizing, @BetterImageSpan.BetterImageSpanAlignment int verticalAlignment) { DraweeHolder draweeHolder = DraweeHolder.create(draweeHierarchy, context); draweeHolder.setController(draweeController); setImageSpan( draweeHolder, startIndex, endIndex, drawableWidthPx, drawableHeightPx, enableResizing, verticalAlignment); } public void setDraweeSpanChangedListener(DraweeSpanChangedListener draweeSpanChangedListener) { mDraweeSpanChangedListener = draweeSpanChangedListener; } public boolean hasDraweeSpans() { return !mDraweeSpans.isEmpty(); } @Override public void onAttachToView(View view) { bindToView(view); onAttach(); } @Override public void onDetachFromView(View view) { unbindFromView(view); onDetach(); } @VisibleForTesting void onAttach() { for (DraweeSpan span : mDraweeSpans) { span.onAttach(); } } @VisibleForTesting void onDetach() { for (DraweeSpan span : mDraweeSpans) { span.onDetach(); } } @VisibleForTesting public Set<DraweeSpan> getDraweeSpans() { return mDraweeSpans; } protected void bindToView(View view) { unbindFromPreviousComponent(); mBoundView = view; } protected void bindToDrawable(Drawable drawable) { unbindFromPreviousComponent(); mBoundDrawable = drawable; } protected void unbindFromView(View view) { if (view != mBoundView) { return; // we are bound to a different view already } mBoundView = null; } protected void unbindFromDrawable(Drawable drawable) { if (drawable != mBoundDrawable) { return; // we are bound to a different view already } mBoundDrawable = null; } protected void unbindFromPreviousComponent() { if (mBoundView != null) { unbindFromView(mBoundView); } if (mBoundDrawable != null) { unbindFromDrawable(mBoundDrawable); } } private class DrawableCallback implements Drawable.Callback { @Override public void invalidateDrawable(Drawable who) { if (mBoundView != null) { // invalidateDrawable might not work correctly since we don't know the exact location // of the drawable and invalidateDrawable could mark the wrong rect as dirty mBoundView.invalidate(); } else if (mBoundDrawable != null) { mBoundDrawable.invalidateSelf(); } } @Override public void scheduleDrawable(Drawable who, Runnable what, long when) { if (mBoundView != null) { mBoundView.scheduleDrawable(who, what, when); } else if (mBoundDrawable != null) { mBoundDrawable.scheduleSelf(what, when); } } @Override public void unscheduleDrawable(Drawable who, Runnable what) { if (mBoundView != null) { unscheduleDrawable(who, what); } else if (mBoundDrawable != null) { mBoundDrawable.unscheduleSelf(what); } } } private class DrawableChangedListener extends BaseControllerListener<ImageInfo> { private final DraweeSpan mDraweeSpan; private final boolean mEnableResizing; private final int mFixedHeight; public DrawableChangedListener(DraweeSpan draweeSpan) { this(draweeSpan, false); } public DrawableChangedListener(DraweeSpan draweeSpan, boolean enableResizing) { this(draweeSpan, enableResizing, UNSET_SIZE); } /** * Create a new DrawableChangedListener If resizing is enabled, the drawable will be resized to * the size of the actual image once it is available. If a fixed height is given and resizing is * enabled, the drawable will be resized to match the aspect ratio of the original image but * will have the given fixed height. * * @param draweeSpan the Drawee span to listen to * @param enableResizing if true, the drawable will be resized according to the final image * size * @param fixedHeight use a fixed height even if resizing is enabled {@link #UNSET_SIZE} */ public DrawableChangedListener( DraweeSpan draweeSpan, boolean enableResizing, int fixedHeight) { Preconditions.checkNotNull(draweeSpan); mDraweeSpan = draweeSpan; mEnableResizing = enableResizing; mFixedHeight = fixedHeight; } @Override public void onFinalImageSet( String id, ImageInfo imageInfo, Animatable animatable) { if (mEnableResizing && imageInfo != null && mDraweeSpan.getDraweeHolder().getTopLevelDrawable() != null) { Drawable topLevelDrawable = mDraweeSpan.getDraweeHolder().getTopLevelDrawable(); Rect topLevelDrawableBounds = topLevelDrawable.getBounds(); if (mFixedHeight != UNSET_SIZE) { float imageWidth = ((float) mFixedHeight / imageInfo.getHeight()) * imageInfo.getWidth(); int imageWidthPx = (int) imageWidth; if (topLevelDrawableBounds.width() != imageWidthPx || topLevelDrawableBounds.height() != mFixedHeight) { topLevelDrawable.setBounds(0, 0, imageWidthPx, mFixedHeight); if (mDraweeSpanChangedListener != null) { mDraweeSpanChangedListener.onDraweeSpanChanged(DraweeSpanStringBuilder.this); } } } else if (topLevelDrawableBounds.width() != imageInfo.getWidth() || topLevelDrawableBounds.height() != imageInfo.getHeight()) { topLevelDrawable.setBounds(0, 0, imageInfo.getWidth(), imageInfo.getHeight()); if (mDraweeSpanChangedListener != null) { mDraweeSpanChangedListener.onDraweeSpanChanged(DraweeSpanStringBuilder.this); } } } } } }