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