/* * 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.view; import android.app.Activity; import android.content.Context; import android.graphics.drawable.Drawable; import android.view.MotionEvent; import android.view.View; import com.facebook.common.activitylistener.ActivityListener; import com.facebook.common.activitylistener.BaseActivityListener; import com.facebook.common.activitylistener.ListenableActivity; import com.facebook.common.internal.Objects; import com.facebook.common.internal.Preconditions; import com.facebook.common.logging.FLog; import com.facebook.drawee.components.DraweeEventTracker; import com.facebook.drawee.drawable.VisibilityAwareDrawable; import com.facebook.drawee.drawable.VisibilityCallback; import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.interfaces.DraweeHierarchy; import javax.annotation.Nullable; import static com.facebook.drawee.components.DraweeEventTracker.Event; /** * A holder class for Drawee controller and hierarchy. * * <p>Drawee users, should, as a rule, use {@link DraweeView} or its subclasses. There are * situations where custom views are required, however, and this class is for those circumstances. * * <p>Each {@link DraweeHierarchy} object should be contained in a single instance of this * class. * * <p>Users of this class must call {@link Drawable#setBounds} on the top-level drawable * of the DraweeHierarchy. Otherwise the drawable will not be drawn. * * <p>The containing view must also call {@link #onDetach()} from its * {@link View#onStartTemporaryDetach()} and {@link View#onDetachedFromWindow()} methods. It must * call {@link #onAttach} from its {@link View#onFinishTemporaryDetach()} and * {@link View#onAttachedToWindow()} methods. */ public class DraweeHolder<DH extends DraweeHierarchy> implements VisibilityCallback { private boolean mIsControllerAttached = false; private boolean mIsHolderAttached = false; private boolean mIsVisible = true; private boolean mIsActivityStarted = true; private DH mHierarchy; private DraweeController mController = null; private final ActivityListener mActivityListener; private final DraweeEventTracker mEventTracker = new DraweeEventTracker(); /** * Creates a new instance of DraweeHolder that detaches / attaches controller whenever context * notifies it about activity's onStop and onStart callbacks. * * <p>It is strongly recommended to pass a {@link ListenableActivity} as context. The holder will * then also be able to respond to onStop and onStart events from that activity, making sure the * image does not waste memory when the activity is stopped. */ public static <DH extends DraweeHierarchy> DraweeHolder<DH> create( @Nullable DH hierarchy, Context context) { DraweeHolder<DH> holder = new DraweeHolder<DH>(hierarchy); holder.registerWithContext(context); return holder; } /** * If the given context is an instance of FbListenableActivity, then listener for its onStop and * onStart methods is registered that changes visibility of the holder. */ public void registerWithContext(Context context) { // TODO(T6181423): this is not working reliably and we cannot afford photos-not-loading issues. //ActivityListenerManager.register(mActivityListener, context); } /** * Creates a new instance of DraweeHolder. * @param hierarchy */ public DraweeHolder(@Nullable DH hierarchy) { if (hierarchy != null) { setHierarchy(hierarchy); } mActivityListener = new BaseActivityListener() { @Override public void onStart(Activity activity) { setActivityStarted(true); } @Override public void onStop(Activity activity) { setActivityStarted(false); } }; } /** * Gets the controller ready to display the image. * * <p>The containing view must call this method from both {@link View#onFinishTemporaryDetach()} * and {@link View#onAttachedToWindow()}. */ public void onAttach() { mEventTracker.recordEvent(Event.ON_HOLDER_ATTACH); mIsHolderAttached = true; attachOrDetatchController(); } /** * Releases resources used to display the image. * * <p>The containing view must call this method from both {@link View#onStartTemporaryDetach()} * and {@link View#onDetachedFromWindow()}. */ public void onDetach() { mEventTracker.recordEvent(Event.ON_HOLDER_DETACH); mIsHolderAttached = false; attachOrDetatchController(); } /** * Forwards the touch event to the controller. * @param event touch event to handle * @return whether the event was handled or not */ public boolean onTouchEvent(MotionEvent event) { if (mController == null) { return false; } return mController.onTouchEvent(event); } /** * Callback used to notify about top-level-drawable's visibility changes. */ @Override public void onVisibilityChange(boolean isVisible) { if (mIsVisible == isVisible) { return; } mEventTracker.recordEvent(isVisible ? Event.ON_DRAWABLE_SHOW : Event.ON_DRAWABLE_HIDE); mIsVisible = isVisible; attachOrDetatchController(); } /** * Callback used to notify about top-level-drawable being drawn. */ @Override public void onDraw() { // draw is only expected if the controller is attached if (mIsControllerAttached) { return; } // something went wrong here; controller is not attached, yet the hierarchy has to be drawn // log error and attach the controller FLog.wtf( DraweeEventTracker.class, "%x: Draw requested for a non-attached controller %x. %s", System.identityHashCode(this), System.identityHashCode(mController), toString()); mIsHolderAttached = true; mIsVisible = true; mIsActivityStarted = true; attachOrDetatchController(); } /** * Sets the visibility callback to the current top-level-drawable. */ private void setVisibilityCallback(@Nullable VisibilityCallback visibilityCallback) { Drawable drawable = getTopLevelDrawable(); if (drawable instanceof VisibilityAwareDrawable) { ((VisibilityAwareDrawable) drawable).setVisibilityCallback(visibilityCallback); } } /** * Notifies the holder of activity's visibility change */ private void setActivityStarted(boolean isStarted) { mEventTracker.recordEvent(isStarted ? Event.ON_ACTIVITY_START : Event.ON_ACTIVITY_STOP); mIsActivityStarted = isStarted; attachOrDetatchController(); } /** * Sets a new controller. */ public void setController(@Nullable DraweeController draweeController) { boolean wasAttached = mIsControllerAttached; if (wasAttached) { detatchController(); } // Clear the old controller if (mController != null) { mEventTracker.recordEvent(Event.ON_CLEAR_OLD_CONTROLLER); mController.setHierarchy(null); } mController = draweeController; if (mController != null) { mEventTracker.recordEvent(Event.ON_SET_CONTROLLER); mController.setHierarchy(mHierarchy); } else { mEventTracker.recordEvent(Event.ON_CLEAR_CONTROLLER); } if (wasAttached) { attachController(); } } /** * Gets the controller if set, null otherwise. */ @Nullable public DraweeController getController() { return mController; } /** * Sets the drawee hierarchy. */ public void setHierarchy(DH hierarchy) { mEventTracker.recordEvent(Event.ON_SET_HIERARCHY); setVisibilityCallback(null); mHierarchy = Preconditions.checkNotNull(hierarchy); onVisibilityChange(mHierarchy.getTopLevelDrawable().isVisible()); setVisibilityCallback(this); if (mController != null) { mController.setHierarchy(hierarchy); } } /** * Gets the drawee hierarchy if set, throws NPE otherwise. */ public DH getHierarchy() { return Preconditions.checkNotNull(mHierarchy); } /** * Returns whether the hierarchy is set or not. */ public boolean hasHierarchy() { return mHierarchy != null; } /** * Gets the top-level drawable if hierarchy is set, null otherwise. */ public Drawable getTopLevelDrawable() { return mHierarchy == null ? null : mHierarchy.getTopLevelDrawable(); } private void attachController() { if (mIsControllerAttached) { return; } mEventTracker.recordEvent(Event.ON_ATTACH_CONTROLLER); mIsControllerAttached = true; if (mController != null && mController.getHierarchy() != null) { mController.onAttach(); } } private void detatchController() { if (!mIsControllerAttached) { return; } mEventTracker.recordEvent(Event.ON_DETACH_CONTROLLER); mIsControllerAttached = false; if (mController != null) { mController.onDetach(); } } private void attachOrDetatchController() { if (mIsHolderAttached && mIsVisible && mIsActivityStarted) { attachController(); } else { detatchController(); } } @Override public String toString() { return Objects.toStringHelper(this) .add("controllerAttached", mIsControllerAttached) .add("holderAttached", mIsHolderAttached) .add("drawableVisible", mIsVisible) .add("activityStarted", mIsActivityStarted) .add("events", mEventTracker.toString()) .toString(); } }