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