1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package androidx.leanback.widget; 15 16 import android.app.Activity; 17 import android.graphics.Matrix; 18 import android.os.Handler; 19 import android.text.TextUtils; 20 import android.util.Log; 21 import android.view.View; 22 import android.view.View.MeasureSpec; 23 import android.view.ViewGroup; 24 import android.widget.ImageView; 25 import android.widget.ImageView.ScaleType; 26 27 import androidx.core.app.ActivityCompat; 28 import androidx.core.app.SharedElementCallback; 29 import androidx.core.view.ViewCompat; 30 import androidx.leanback.transition.TransitionHelper; 31 import androidx.leanback.transition.TransitionListener; 32 import androidx.leanback.widget.DetailsOverviewRowPresenter.ViewHolder; 33 34 import java.lang.ref.WeakReference; 35 import java.util.List; 36 37 final class DetailsOverviewSharedElementHelper extends SharedElementCallback { 38 39 static final String TAG = "DetailsTransitionHelper"; 40 static final boolean DEBUG = false; 41 42 static class TransitionTimeOutRunnable implements Runnable { 43 WeakReference<DetailsOverviewSharedElementHelper> mHelperRef; 44 TransitionTimeOutRunnable(DetailsOverviewSharedElementHelper helper)45 TransitionTimeOutRunnable(DetailsOverviewSharedElementHelper helper) { 46 mHelperRef = new WeakReference<DetailsOverviewSharedElementHelper>(helper); 47 } 48 49 @Override run()50 public void run() { 51 DetailsOverviewSharedElementHelper helper = mHelperRef.get(); 52 if (helper == null) { 53 return; 54 } 55 if (DEBUG) { 56 Log.d(TAG, "timeout " + helper.mActivityToRunTransition); 57 } 58 helper.startPostponedEnterTransition(); 59 } 60 } 61 62 ViewHolder mViewHolder; 63 Activity mActivityToRunTransition; 64 boolean mStartedPostpone; 65 String mSharedElementName; 66 int mRightPanelWidth; 67 int mRightPanelHeight; 68 69 private ScaleType mSavedScaleType; 70 private Matrix mSavedMatrix; 71 hasImageViewScaleChange(View snapshotView)72 private boolean hasImageViewScaleChange(View snapshotView) { 73 return snapshotView instanceof ImageView; 74 } 75 saveImageViewScale()76 private void saveImageViewScale() { 77 if (mSavedScaleType == null) { 78 // only save first time after initialize/restoreImageViewScale() 79 ImageView imageView = mViewHolder.mImageView; 80 mSavedScaleType = imageView.getScaleType(); 81 mSavedMatrix = mSavedScaleType == ScaleType.MATRIX ? imageView.getMatrix() : null; 82 if (DEBUG) { 83 Log.d(TAG, "saveImageViewScale: "+mSavedScaleType); 84 } 85 } 86 } 87 updateImageViewAfterScaleTypeChange(ImageView imageView)88 private static void updateImageViewAfterScaleTypeChange(ImageView imageView) { 89 // enforcing imageView to update its internal bounds/matrix immediately 90 imageView.measure( 91 MeasureSpec.makeMeasureSpec(imageView.getMeasuredWidth(), MeasureSpec.EXACTLY), 92 MeasureSpec.makeMeasureSpec(imageView.getMeasuredHeight(), MeasureSpec.EXACTLY)); 93 imageView.layout(imageView.getLeft(), imageView.getTop(), 94 imageView.getRight(), imageView.getBottom()); 95 } 96 changeImageViewScale(View snapshotView)97 private void changeImageViewScale(View snapshotView) { 98 ImageView snapshotImageView = (ImageView) snapshotView; 99 ImageView imageView = mViewHolder.mImageView; 100 if (DEBUG) { 101 Log.d(TAG, "changeImageViewScale to "+snapshotImageView.getScaleType()); 102 } 103 imageView.setScaleType(snapshotImageView.getScaleType()); 104 if (snapshotImageView.getScaleType() == ScaleType.MATRIX) { 105 imageView.setImageMatrix(snapshotImageView.getImageMatrix()); 106 } 107 updateImageViewAfterScaleTypeChange(imageView); 108 } 109 restoreImageViewScale()110 private void restoreImageViewScale() { 111 if (mSavedScaleType != null) { 112 if (DEBUG) { 113 Log.d(TAG, "restoreImageViewScale to "+mSavedScaleType); 114 } 115 ImageView imageView = mViewHolder.mImageView; 116 imageView.setScaleType(mSavedScaleType); 117 if (mSavedScaleType == ScaleType.MATRIX) { 118 imageView.setImageMatrix(mSavedMatrix); 119 } 120 // only restore once unless another save happens 121 mSavedScaleType = null; 122 updateImageViewAfterScaleTypeChange(imageView); 123 } 124 } 125 126 @Override onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots)127 public void onSharedElementStart(List<String> sharedElementNames, 128 List<View> sharedElements, List<View> sharedElementSnapshots) { 129 if (DEBUG) { 130 Log.d(TAG, "onSharedElementStart " + mActivityToRunTransition); 131 } 132 if (sharedElements.size() < 1) { 133 return; 134 } 135 View overviewView = sharedElements.get(0); 136 if (mViewHolder == null || mViewHolder.mOverviewFrame != overviewView) { 137 return; 138 } 139 View snapshot = sharedElementSnapshots.get(0); 140 if (hasImageViewScaleChange(snapshot)) { 141 saveImageViewScale(); 142 changeImageViewScale(snapshot); 143 } 144 View imageView = mViewHolder.mImageView; 145 final int width = overviewView.getWidth(); 146 final int height = overviewView.getHeight(); 147 imageView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 148 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); 149 imageView.layout(0, 0, width, height); 150 final View rightPanel = mViewHolder.mRightPanel; 151 if (mRightPanelWidth != 0 && mRightPanelHeight != 0) { 152 rightPanel.measure(MeasureSpec.makeMeasureSpec(mRightPanelWidth, MeasureSpec.EXACTLY), 153 MeasureSpec.makeMeasureSpec(mRightPanelHeight, MeasureSpec.EXACTLY)); 154 rightPanel.layout(width, rightPanel.getTop(), width + mRightPanelWidth, 155 rightPanel.getTop() + mRightPanelHeight); 156 } else { 157 rightPanel.offsetLeftAndRight(width - rightPanel.getLeft()); 158 } 159 mViewHolder.mActionsRow.setVisibility(View.INVISIBLE); 160 mViewHolder.mDetailsDescriptionFrame.setVisibility(View.INVISIBLE); 161 } 162 163 @Override onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots)164 public void onSharedElementEnd(List<String> sharedElementNames, 165 List<View> sharedElements, List<View> sharedElementSnapshots) { 166 if (DEBUG) { 167 Log.d(TAG, "onSharedElementEnd " + mActivityToRunTransition); 168 } 169 if (sharedElements.size() < 1) { 170 return; 171 } 172 View overviewView = sharedElements.get(0); 173 if (mViewHolder == null || mViewHolder.mOverviewFrame != overviewView) { 174 return; 175 } 176 restoreImageViewScale(); 177 // temporary let action row take focus so we defer button background animation 178 mViewHolder.mActionsRow.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 179 mViewHolder.mActionsRow.setVisibility(View.VISIBLE); 180 mViewHolder.mActionsRow.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); 181 // switch focusability to VISIBLE wont trigger focusableViewAvailable() on O because 182 // shared element details_frame is still INVISIBLE. b/63544781 183 mViewHolder.mActionsRow.requestFocus(); 184 mViewHolder.mDetailsDescriptionFrame.setVisibility(View.VISIBLE); 185 } 186 setSharedElementEnterTransition(Activity activity, String sharedElementName, long timeoutMs)187 void setSharedElementEnterTransition(Activity activity, String sharedElementName, 188 long timeoutMs) { 189 if ((activity == null && !TextUtils.isEmpty(sharedElementName)) 190 || (activity != null && TextUtils.isEmpty(sharedElementName))) { 191 throw new IllegalArgumentException(); 192 } 193 if (activity == mActivityToRunTransition 194 && TextUtils.equals(sharedElementName, mSharedElementName)) { 195 return; 196 } 197 if (mActivityToRunTransition != null) { 198 ActivityCompat.setEnterSharedElementCallback(mActivityToRunTransition, null); 199 } 200 mActivityToRunTransition = activity; 201 mSharedElementName = sharedElementName; 202 if (DEBUG) { 203 Log.d(TAG, "postponeEnterTransition " + mActivityToRunTransition); 204 } 205 ActivityCompat.setEnterSharedElementCallback(mActivityToRunTransition, this); 206 ActivityCompat.postponeEnterTransition(mActivityToRunTransition); 207 if (timeoutMs > 0) { 208 new Handler().postDelayed(new TransitionTimeOutRunnable(this), timeoutMs); 209 } 210 } 211 onBindToDrawable(ViewHolder vh)212 void onBindToDrawable(ViewHolder vh) { 213 if (DEBUG) { 214 Log.d(TAG, "onBindToDrawable, could start transition of " + mActivityToRunTransition); 215 } 216 if (mViewHolder != null) { 217 if (DEBUG) { 218 Log.d(TAG, "rebind? clear transitionName on current viewHolder " 219 + mViewHolder.mOverviewFrame); 220 } 221 ViewCompat.setTransitionName(mViewHolder.mOverviewFrame, null); 222 } 223 // After we got a image drawable, we can determine size of right panel. 224 // We want right panel to have fixed size so that the right panel don't change size 225 // when the overview is layout as a small bounds in transition. 226 mViewHolder = vh; 227 mViewHolder.mRightPanel.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 228 @Override 229 public void onLayoutChange(View v, int left, int top, int right, int bottom, 230 int oldLeft, int oldTop, int oldRight, int oldBottom) { 231 mViewHolder.mRightPanel.removeOnLayoutChangeListener(this); 232 mRightPanelWidth = mViewHolder.mRightPanel.getWidth(); 233 mRightPanelHeight = mViewHolder.mRightPanel.getHeight(); 234 if (DEBUG) { 235 Log.d(TAG, "onLayoutChange records size of right panel as " 236 + mRightPanelWidth + ", "+ mRightPanelHeight); 237 } 238 } 239 }); 240 mViewHolder.mRightPanel.postOnAnimation(new Runnable() { 241 @Override 242 public void run() { 243 if (DEBUG) { 244 Log.d(TAG, "setTransitionName "+mViewHolder.mOverviewFrame); 245 } 246 ViewCompat.setTransitionName(mViewHolder.mOverviewFrame, mSharedElementName); 247 Object transition = TransitionHelper.getSharedElementEnterTransition( 248 mActivityToRunTransition.getWindow()); 249 if (transition != null) { 250 TransitionHelper.addTransitionListener(transition, new TransitionListener() { 251 @Override 252 public void onTransitionEnd(Object transition) { 253 if (DEBUG) { 254 Log.d(TAG, "onTransitionEnd " + mActivityToRunTransition); 255 } 256 // after transition if the action row still focused, transfer 257 // focus to its children 258 if (mViewHolder.mActionsRow.isFocused()) { 259 mViewHolder.mActionsRow.requestFocus(); 260 } 261 TransitionHelper.removeTransitionListener(transition, this); 262 } 263 }); 264 } 265 startPostponedEnterTransition(); 266 } 267 }); 268 } 269 startPostponedEnterTransition()270 void startPostponedEnterTransition() { 271 if (!mStartedPostpone) { 272 if (DEBUG) { 273 Log.d(TAG, "startPostponedEnterTransition " + mActivityToRunTransition); 274 } 275 ActivityCompat.startPostponedEnterTransition(mActivityToRunTransition); 276 mStartedPostpone = true; 277 } 278 } 279 } 280