1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tv.settings.dialog;
18
19import android.animation.Animator;
20import android.animation.AnimatorInflater;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.animation.TimeInterpolator;
25import android.app.Fragment;
26import android.app.FragmentManager;
27import android.app.FragmentTransaction;
28import android.content.res.Resources;
29import android.graphics.Bitmap;
30import android.graphics.Color;
31import android.graphics.drawable.ColorDrawable;
32import android.graphics.drawable.Drawable;
33import android.net.Uri;
34import android.os.Bundle;
35import android.os.Handler;
36import android.support.v17.leanback.widget.VerticalGridView;
37import android.support.v4.view.ViewCompat;
38import android.support.v7.widget.RecyclerView;
39import android.view.LayoutInflater;
40import android.view.View;
41import android.view.ViewGroup;
42import android.view.ViewGroup.LayoutParams;
43import android.view.ViewPropertyAnimator;
44import android.view.ViewTreeObserver;
45import android.view.animation.DecelerateInterpolator;
46import android.widget.ImageView;
47import android.widget.TextView;
48
49import com.android.tv.settings.R;
50import com.android.tv.settings.util.AccessibilityHelper;
51
52import java.util.ArrayList;
53
54/**
55 * Displays content on the left and actions on the right.
56 */
57public class SettingsLayoutFragment extends Fragment implements Layout.LayoutNodeRefreshListener {
58
59    public static final String TAG_LEAN_BACK_DIALOG_FRAGMENT = "leanBackSettingsLayoutFragment";
60    private static final String EXTRA_CONTENT_TITLE = "title";
61    private static final String EXTRA_CONTENT_BREADCRUMB = "breadcrumb";
62    private static final String EXTRA_CONTENT_DESCRIPTION = "description";
63    private static final String EXTRA_CONTENT_ICON = "icon";
64    private static final String EXTRA_CONTENT_ICON_URI = "iconUri";
65    private static final String EXTRA_CONTENT_ICON_BITMAP = "iconBitmap";
66    private static final String EXTRA_CONTENT_ICON_BACKGROUND = "iconBackground";
67    private static final String EXTRA_ACTION_NAME = "name";
68    private static final String EXTRA_ACTION_SELECTED_INDEX = "selectedIndex";
69    private static final String EXTRA_ENTRY_TRANSITION_PERFORMED = "entryTransitionPerformed";
70    private static final int ANIMATION_FRAGMENT_ENTER = 1;
71    private static final int ANIMATION_FRAGMENT_EXIT = 2;
72    private static final int ANIMATION_FRAGMENT_ENTER_POP = 3;
73    private static final int ANIMATION_FRAGMENT_EXIT_POP = 4;
74    private static final float WINDOW_ALIGNMENT_OFFSET_PERCENT = 50f;
75    private static final float FADE_IN_ALPHA_START = 0f;
76    private static final float FADE_IN_ALPHA_FINISH = 1f;
77    private static final float SLIDE_OUT_ANIMATOR_LEFT = 0f;
78    private static final float SLIDE_OUT_ANIMATOR_RIGHT = 200f;
79    private static final float SLIDE_OUT_ANIMATOR_START_ALPHA = 0f;
80    private static final float SLIDE_OUT_ANIMATOR_END_ALPHA = 1f;
81
82    public interface Listener {
83        void onActionClicked(Layout.Action action);
84    }
85
86    /**
87     * Builds a SettingsLayoutFragment object.
88     */
89    public static class Builder {
90
91        private String mContentTitle;
92        private String mContentBreadcrumb;
93        private String mContentDescription;
94        private Drawable mIcon;
95        private Uri mIconUri;
96        private Bitmap mIconBitmap;
97        private int mIconBackgroundColor = Color.TRANSPARENT;
98        private String mName;
99
100        public SettingsLayoutFragment build() {
101            SettingsLayoutFragment fragment = new SettingsLayoutFragment();
102            Bundle args = new Bundle();
103            args.putString(EXTRA_CONTENT_TITLE, mContentTitle);
104            args.putString(EXTRA_CONTENT_BREADCRUMB, mContentBreadcrumb);
105            args.putString(EXTRA_CONTENT_DESCRIPTION, mContentDescription);
106            //args.putParcelable(EXTRA_CONTENT_ICON, mIcon);
107            fragment.mIcon = mIcon;
108            args.putParcelable(EXTRA_CONTENT_ICON_URI, mIconUri);
109            args.putParcelable(EXTRA_CONTENT_ICON_BITMAP, mIconBitmap);
110            args.putInt(EXTRA_CONTENT_ICON_BACKGROUND, mIconBackgroundColor);
111            args.putString(EXTRA_ACTION_NAME, mName);
112            fragment.setArguments(args);
113            return fragment;
114        }
115
116        public Builder title(String title) {
117            mContentTitle = title;
118            return this;
119        }
120
121        public Builder breadcrumb(String breadcrumb) {
122            mContentBreadcrumb = breadcrumb;
123            return this;
124        }
125
126        public Builder description(String description) {
127            mContentDescription = description;
128            return this;
129        }
130
131        public Builder icon(Drawable icon) {
132            mIcon = icon;
133            return this;
134        }
135
136        public Builder iconUri(Uri iconUri) {
137            mIconUri = iconUri;
138            return this;
139        }
140
141        public Builder iconBitmap(Bitmap iconBitmap) {
142            mIconBitmap = iconBitmap;
143            return this;
144        }
145
146        public Builder iconBackgroundColor(int iconBackgroundColor) {
147            mIconBackgroundColor = iconBackgroundColor;
148            return this;
149        }
150
151        public Builder name(String name) {
152            mName = name;
153            return this;
154        }
155    }
156
157    public static void add(FragmentManager fm, SettingsLayoutFragment f) {
158        boolean hasDialog = fm.findFragmentByTag(TAG_LEAN_BACK_DIALOG_FRAGMENT) != null;
159        FragmentTransaction ft = fm.beginTransaction();
160
161        if (hasDialog) {
162            ft.setCustomAnimations(ANIMATION_FRAGMENT_ENTER,
163                    ANIMATION_FRAGMENT_EXIT, ANIMATION_FRAGMENT_ENTER_POP,
164                    ANIMATION_FRAGMENT_EXIT_POP);
165            ft.addToBackStack(null);
166        }
167        ft.replace(android.R.id.content, f, TAG_LEAN_BACK_DIALOG_FRAGMENT).commit();
168    }
169
170    private SettingsLayoutAdapter mAdapter;
171    private VerticalGridView mListView;
172    private String mTitle;
173    private String mBreadcrumb;
174    private String mDescription;
175    private Drawable mIcon;
176    private Uri mIconUri;
177    private Bitmap mIconBitmap;
178    private int mIconBackgroundColor = Color.TRANSPARENT;
179    private Layout mLayout;
180    private String mName;
181    private int mSelectedIndex = -1;
182    private boolean mEntryTransitionPerformed;
183    private boolean mIntroAnimationInProgress;
184    private int mAnimateInDuration;
185    private int mAnimateDelay;
186    private int mSecondaryAnimateDelay;
187    private int mSlideInStagger;
188    private int mSlideInDistance;
189    private final Handler refreshViewHandler = new Handler();
190
191    private final Runnable mRefreshViewRunnable = new Runnable() {
192        @Override
193        public void run() {
194            if (isResumed()) {
195                mLayout.setSelectedIndex(mListView.getSelectedPosition());
196                mLayout.reloadLayoutRows();
197                mAdapter.setLayoutRows(mLayout.getLayoutRows());
198                mAdapter.setNoAnimateMode();
199                mAdapter.notifyDataSetChanged();
200                mListView.setSelectedPositionSmooth(mLayout.getSelectedIndex());
201            }
202        }
203    };
204
205    private final SettingsLayoutAdapter.Listener mLayoutViewRowClicked =
206        new SettingsLayoutAdapter.Listener() {
207            @Override
208            public void onRowClicked(Layout.LayoutRow layoutRow) {
209                onRowViewClicked(layoutRow);
210            }
211        };
212
213    private final SettingsLayoutAdapter.OnFocusListener mLayoutViewOnFocus =
214        new SettingsLayoutAdapter.OnFocusListener() {
215            @Override
216            public void onActionFocused(Layout.LayoutRow action) {
217                if (getActivity() instanceof SettingsLayoutAdapter.OnFocusListener) {
218                    SettingsLayoutAdapter.OnFocusListener listener =
219                            (SettingsLayoutAdapter.OnFocusListener) getActivity();
220                    listener.onActionFocused(action);
221                }
222            }
223        };
224
225    @Override
226    public void onCreate(Bundle savedInstanceState) {
227        super.onCreate(savedInstanceState);
228        Bundle state = (savedInstanceState != null) ? savedInstanceState : getArguments();
229        mTitle = state.getString(EXTRA_CONTENT_TITLE);
230        mBreadcrumb = state.getString(EXTRA_CONTENT_BREADCRUMB);
231        mDescription = state.getString(EXTRA_CONTENT_DESCRIPTION);
232        //mIcon = state.getParcelable(EXTRA_CONTENT_ICON_RESOURCE_ID, 0);
233        mIconUri = state.getParcelable(EXTRA_CONTENT_ICON_URI);
234        mIconBitmap = state.getParcelable(EXTRA_CONTENT_ICON_BITMAP);
235        mIconBackgroundColor = state.getInt(EXTRA_CONTENT_ICON_BACKGROUND, Color.TRANSPARENT);
236        mName = state.getString(EXTRA_ACTION_NAME);
237        mSelectedIndex = state.getInt(EXTRA_ACTION_SELECTED_INDEX, -1);
238        mEntryTransitionPerformed = state.getBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, false);
239    }
240
241    @Override
242    public View onCreateView(LayoutInflater inflater, ViewGroup container,
243            Bundle savedInstanceState) {
244
245        View v = inflater.inflate(R.layout.lb_dialog_fragment, container, false);
246
247        View contentContainer = v.findViewById(R.id.content_fragment);
248        View content = inflater.inflate(R.layout.lb_dialog_content, container, false);
249        ((ViewGroup) contentContainer).addView(content);
250        initializeContentView(content);
251        v.setTag(R.id.content_fragment, content);
252
253        View actionContainer = v.findViewById(R.id.action_fragment);
254        View action = inflater.inflate(R.layout.lb_dialog_action_list, container, false);
255        ((ViewGroup) actionContainer).addView(action);
256        setActionView(action);
257        v.setTag(R.id.action_fragment, action);
258
259        Resources res = getActivity().getResources();
260        mAnimateInDuration = res.getInteger(R.integer.animate_in_duration);
261        mAnimateDelay = res.getInteger(R.integer.animate_delay);
262        mSecondaryAnimateDelay = res.getInteger(R.integer.secondary_animate_delay);
263        mSlideInStagger = res.getInteger(R.integer.slide_in_stagger);
264        mSlideInDistance = res.getInteger(R.integer.slide_in_distance);
265
266        return v;
267    }
268
269    @Override
270    public void onSaveInstanceState(Bundle outState) {
271        super.onSaveInstanceState(outState);
272        outState.putString(EXTRA_CONTENT_TITLE, mTitle);
273        outState.putString(EXTRA_CONTENT_BREADCRUMB, mBreadcrumb);
274        outState.putString(EXTRA_CONTENT_DESCRIPTION, mDescription);
275        //outState.putInt(EXTRA_CONTENT_ICON_RESOURCE_ID, mIconResourceId);
276        outState.putParcelable(EXTRA_CONTENT_ICON_URI, mIconUri);
277        outState.putParcelable(EXTRA_CONTENT_ICON_BITMAP, mIconBitmap);
278        outState.putInt(EXTRA_CONTENT_ICON_BACKGROUND, mIconBackgroundColor);
279        outState.putInt(EXTRA_ACTION_SELECTED_INDEX,
280                (mListView != null) ? mListView.getSelectedPosition() : -1);
281        outState.putString(EXTRA_ACTION_NAME, mName);
282        outState.putBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, mEntryTransitionPerformed);
283    }
284
285    @Override
286    public void onStart() {
287        super.onStart();
288        if (!mEntryTransitionPerformed) {
289            mEntryTransitionPerformed = true;
290            performEntryTransition();
291        } else {
292            final View dialogView = getView();
293            final View contentView = (View) dialogView.getTag(R.id.content_fragment);
294
295            int bgColor = contentView.getContext().getColor(R.color.lb_dialog_activity_background);
296            final ColorDrawable bgDrawable = new ColorDrawable();
297            bgDrawable.setColor(bgColor);
298            dialogView.setBackground(bgDrawable);
299        }
300    }
301
302    public void setLayout(Layout layout) {
303        mLayout = layout;
304        mLayout.setRefreshViewListener(this);
305    }
306
307    // TODO refactor to get this call as the result of a callback from the Layout.
308    private void updateViews() {
309        View dialogView = getView();
310        View contentView = (View) dialogView.getTag(R.id.content_fragment);
311
312        mBreadcrumb = mLayout.getBreadcrumb();
313        TextView breadcrumbView = (TextView) contentView.getTag(R.id.breadcrumb);
314        breadcrumbView.setText(mBreadcrumb);
315
316        mTitle = mLayout.getTitle();
317        TextView titleView = (TextView) contentView.getTag(R.id.title);
318        titleView.setText(mTitle);
319
320        mDescription = mLayout.getDescription();
321        TextView descriptionView = (TextView) contentView.getTag(R.id.description);
322        descriptionView.setText(mDescription);
323
324        mAdapter.setLayoutRows(mLayout.getLayoutRows());
325        mAdapter.notifyDataSetChanged();
326        mAdapter.setFocusListenerEnabled(false);
327        mListView.setSelectedPosition(mLayout.getSelectedIndex());
328        mAdapter.setFocusListenerEnabled(true);
329    }
330
331    public void setIcon(int resId) {
332        View dialogView = getView();
333        View contentView = (View) dialogView.getTag(R.id.content_fragment);
334        ImageView iconView = (ImageView) contentView.findViewById(R.id.icon);
335        if (iconView != null) {
336            iconView.setImageResource(resId);
337        }
338    }
339
340    /**
341     * Notification that a part of the model antecedent to the visible view has changed.
342     */
343    @Override
344    public void onRefreshView() {
345        refreshViewHandler.removeCallbacks(mRefreshViewRunnable);
346        refreshViewHandler.post(mRefreshViewRunnable);
347    }
348
349    /**
350     * Return the currently selected node. The return value may be null, if this is called before
351     * the layout has been rendered for the first time. Clients should check the return value
352     * before using.
353     */
354    @Override
355    public Layout.Node getSelectedNode() {
356        int index = mListView.getSelectedPosition();
357        ArrayList<Layout.LayoutRow> layoutRows = mLayout.getLayoutRows();
358        if (index < layoutRows.size()) {
359            return layoutRows.get(index).getNode();
360        } else {
361            return null;
362        }
363    }
364
365    /**
366     * Process forward key press.
367     */
368    void onRowViewClicked(Layout.LayoutRow layoutRow) {
369        if (layoutRow.isGoBack()) {
370            onBackPressed();
371        } else if (layoutRow.isRadio()) {
372            if (layoutRow.setRadioSelectedIndex()) {
373                // SelectionGroup selection has changed, notify client.
374                Listener actionListener = (Listener) getActivity();
375                if (actionListener != null) {
376                    // Create a temporary Action to return the id.
377                    actionListener.onActionClicked(new Layout.Action(layoutRow.getRadioId()));
378                }
379            }
380            onBackPressed();
381        } else {
382            Layout.Action action = layoutRow.getUserAction();
383            if (action != null) {
384                Listener actionListener = (Listener) getActivity();
385                if (actionListener != null) {
386                    actionListener.onActionClicked(action);
387                }
388            } else if (mLayout.onClickNavigate(layoutRow)) {
389                mLayout.setParentSelectedIndex(mListView.getSelectedPosition());
390                updateViews();
391            }
392        }
393    }
394
395    /**
396     * Process back key press.
397     */
398    public boolean onBackPressed() {
399        if (mLayout.goBack()) {
400            updateViews();
401            return true;
402        } else {
403            return false;
404        }
405    }
406
407    /**
408     * Client has requested header with {@param title} be selected. If there is no such header
409     * return to the first row.
410     */
411    protected void goBackToTitle(String title) {
412        mLayout.goToTitle(title);
413        updateViews();
414    }
415
416    @Override
417    public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
418        View dialogView = getView();
419        View contentView = (View) dialogView.getTag(R.id.content_fragment);
420        View actionView = (View) dialogView.getTag(R.id.action_fragment);
421        View actionContainerView = dialogView.findViewById(R.id.action_fragment);
422        View titleView = (View) contentView.getTag(R.id.title);
423        View breadcrumbView = (View) contentView.getTag(R.id.breadcrumb);
424        View descriptionView = (View) contentView.getTag(R.id.description);
425        View iconView = (View) contentView.getTag(R.id.icon);
426        View listView = (View) actionView.getTag(R.id.list);
427        View selectorView = (View) actionView.getTag(R.id.selector);
428
429        ArrayList<Animator> animators = new ArrayList<>();
430
431        switch (nextAnim) {
432            case ANIMATION_FRAGMENT_ENTER:
433                animators.add(createSlideInFromEndAnimator(titleView));
434                animators.add(createSlideInFromEndAnimator(breadcrumbView));
435                animators.add(createSlideInFromEndAnimator(descriptionView));
436                animators.add(createSlideInFromEndAnimator(iconView));
437                animators.add(createSlideInFromEndAnimator(listView));
438                animators.add(createSlideInFromEndAnimator(selectorView));
439                break;
440            case ANIMATION_FRAGMENT_EXIT:
441                animators.add(createSlideOutToStartAnimator(titleView));
442                animators.add(createSlideOutToStartAnimator(breadcrumbView));
443                animators.add(createSlideOutToStartAnimator(descriptionView));
444                animators.add(createSlideOutToStartAnimator(iconView));
445                animators.add(createSlideOutToStartAnimator(listView));
446                animators.add(createSlideOutToStartAnimator(selectorView));
447                animators.add(createFadeOutAnimator(actionContainerView));
448                break;
449            case ANIMATION_FRAGMENT_ENTER_POP:
450                animators.add(createSlideInFromStartAnimator(titleView));
451                animators.add(createSlideInFromStartAnimator(breadcrumbView));
452                animators.add(createSlideInFromStartAnimator(descriptionView));
453                animators.add(createSlideInFromStartAnimator(iconView));
454                animators.add(createSlideInFromStartAnimator(listView));
455                animators.add(createSlideInFromStartAnimator(selectorView));
456                break;
457            case ANIMATION_FRAGMENT_EXIT_POP:
458                animators.add(createSlideOutToEndAnimator(titleView));
459                animators.add(createSlideOutToEndAnimator(breadcrumbView));
460                animators.add(createSlideOutToEndAnimator(descriptionView));
461                animators.add(createSlideOutToEndAnimator(iconView));
462                animators.add(createSlideOutToEndAnimator(listView));
463                animators.add(createSlideOutToEndAnimator(selectorView));
464                animators.add(createFadeOutAnimator(actionContainerView));
465                break;
466            default:
467                return super.onCreateAnimator(transit, enter, nextAnim);
468        }
469
470        mEntryTransitionPerformed = true;
471        return createDummyAnimator(dialogView, animators);
472    }
473
474    /**
475     * Called when intro animation is finished.
476     * <p>
477     * If a subclass is going to alter the view, should wait until this is
478     * called.
479     */
480    public void onIntroAnimationFinished() {
481        mIntroAnimationInProgress = false;
482
483        // Display the selector view.
484        View focusedChild = mListView.getFocusedChild();
485        if (focusedChild != null) {
486            View actionView = (View) getView().getTag(R.id.action_fragment);
487            int height = focusedChild.getHeight ();
488            View selectorView = actionView.findViewById(R.id.selector);
489            LayoutParams lp = selectorView.getLayoutParams();
490            lp.height = height;
491            selectorView.setLayoutParams(lp);
492            selectorView.setAlpha (1f);
493        }
494    }
495
496    public boolean isIntroAnimationInProgress() {
497        return mIntroAnimationInProgress;
498    }
499
500    private void initializeContentView(View content) {
501        TextView titleView = (TextView) content.findViewById(R.id.title);
502        TextView breadcrumbView = (TextView) content.findViewById(R.id.breadcrumb);
503        TextView descriptionView = (TextView) content.findViewById(R.id.description);
504        titleView.setText(mTitle);
505        breadcrumbView.setText(mBreadcrumb);
506        descriptionView.setText(mDescription);
507        final ImageView iconImageView = (ImageView) content.findViewById(R.id.icon);
508        iconImageView.setBackgroundColor(mIconBackgroundColor);
509
510        // Force text fields to be focusable when accessibility is enabled.
511        if (AccessibilityHelper.forceFocusableViews(getActivity())) {
512            titleView.setFocusable(true);
513            titleView.setFocusableInTouchMode(true);
514            descriptionView.setFocusable(true);
515            descriptionView.setFocusableInTouchMode(true);
516            breadcrumbView.setFocusable(true);
517            breadcrumbView.setFocusableInTouchMode(true);
518        }
519
520        if (mIcon != null) {
521            iconImageView.setImageDrawable(mIcon);
522            updateViewSize(iconImageView);
523        } else if (mIconBitmap != null) {
524            iconImageView.setImageBitmap(mIconBitmap);
525            updateViewSize(iconImageView);
526        } else if (mIconUri != null) {
527            iconImageView.setVisibility(View.INVISIBLE);
528            /*
529
530            BitmapDownloader bitmapDownloader = BitmapDownloader.getInstance(
531                    content.getContext());
532            mBitmapCallBack = new BitmapCallback() {
533                @Override
534                public void onBitmapRetrieved(Bitmap bitmap) {
535                    if (bitmap != null) {
536                        mIconBitmap = bitmap;
537                        iconImageView.setVisibility(View.VISIBLE);
538                        iconImageView.setImageBitmap(bitmap);
539                        updateViewSize(iconImageView);
540                    }
541                }
542            };
543
544            bitmapDownloader.getBitmap(new BitmapWorkerOptions.Builder(
545                    content.getContext()).resource(mIconUri)
546                    .width(iconImageView.getLayoutParams().width).build(),
547                    mBitmapCallBack);
548            */
549        } else {
550            iconImageView.setVisibility(View.GONE);
551        }
552
553        content.setTag(R.id.title, titleView);
554        content.setTag(R.id.breadcrumb, breadcrumbView);
555        content.setTag(R.id.description, descriptionView);
556        content.setTag(R.id.icon, iconImageView);
557    }
558
559    private void setActionView(View action) {
560        mAdapter = new SettingsLayoutAdapter(mLayoutViewRowClicked, mLayoutViewOnFocus);
561        mAdapter.setLayoutRows(mLayout.getLayoutRows());
562        if (action instanceof VerticalGridView) {
563            mListView = (VerticalGridView) action;
564        } else {
565            mListView = (VerticalGridView) action.findViewById(R.id.list);
566            if (mListView == null) {
567                throw new IllegalArgumentException("No ListView exists.");
568            }
569            mListView.setWindowAlignmentOffset(0);
570            mListView.setWindowAlignmentOffsetPercent(WINDOW_ALIGNMENT_OFFSET_PERCENT);
571            mListView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
572            View selectorView = action.findViewById(R.id.selector);
573            if (selectorView != null) {
574                mListView.setOnScrollListener(new SelectorAnimator(selectorView, mListView));
575            }
576        }
577
578        mListView.requestFocusFromTouch();
579        mListView.setAdapter(mAdapter);
580        int initialSelectedIndex;
581        if (mSelectedIndex >= 0 && mSelectedIndex < mLayout.getLayoutRows().size()) {
582            // "mSelectedIndex" is a valid index and so must have been initialized from a Bundle in
583            // the "onCreate" member and the only way it could be a valid index is if it was saved
584            // by "onSaveInstanceState" since it is initialized to "-1" (an invalid value) in the
585            // constructor.
586            initialSelectedIndex = mSelectedIndex;
587        } else {
588            // First time this fragment is being instantiated, i.e. did not reach here via the
589            // "onSaveInstanceState" route. Initialize the index from the starting index defined
590            // in the "Layout".
591            initialSelectedIndex = mLayout.getSelectedIndex();
592        }
593        mListView.setSelectedPositionSmooth(initialSelectedIndex);
594        action.setTag(R.id.list, mListView);
595        action.setTag(R.id.selector, action.findViewById(R.id.selector));
596    }
597
598    private void updateViewSize(ImageView iconView) {
599        int intrinsicWidth = iconView.getDrawable().getIntrinsicWidth();
600        LayoutParams lp = iconView.getLayoutParams();
601        if (intrinsicWidth > 0) {
602            lp.height = lp.width * iconView.getDrawable().getIntrinsicHeight()
603                    / intrinsicWidth;
604        } else {
605            // If no intrinsic width, then just mke this a square.
606            lp.height = lp.width;
607        }
608    }
609
610    private void fadeIn(View v) {
611        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", FADE_IN_ALPHA_START,
612                FADE_IN_ALPHA_FINISH);
613        alphaAnimator.setDuration(v.getContext().getResources().getInteger(
614                android.R.integer.config_mediumAnimTime));
615        alphaAnimator.start();
616    }
617
618    private void performEntryTransition() {
619        final View dialogView = getView();
620        final View contentView = (View) dialogView.getTag(R.id.content_fragment);
621        final View actionContainerView = dialogView.findViewById(R.id.action_fragment);
622
623        mIntroAnimationInProgress = true;
624
625        // Fade out the old activity.
626        getActivity().overridePendingTransition(0, R.anim.lb_dialog_fade_out);
627
628        int bgColor = contentView.getContext().getColor(R.color.lb_dialog_activity_background);
629        final ColorDrawable bgDrawable = new ColorDrawable();
630        bgDrawable.setColor(bgColor);
631        bgDrawable.setAlpha(0);
632        dialogView.setBackground(bgDrawable);
633        dialogView.setVisibility(View.INVISIBLE);
634
635        // We need to defer the remainder of the animation preparation until the first layout has
636        // occurred, as we don't yet know the final location of the icon.
637        contentView.getViewTreeObserver().addOnGlobalLayoutListener(
638                new ViewTreeObserver.OnGlobalLayoutListener() {
639                @Override
640                    public void onGlobalLayout() {
641                        contentView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
642                        // if we buildLayer() at this time, the texture is
643                        // actually not created delay a little so we can make
644                        // sure all hardware layer is created before animation,
645                        // in that way we can avoid the jittering of start
646                        // animation
647                        contentView.postOnAnimationDelayed(mEntryAnimationRunnable, mAnimateDelay);
648                    }
649
650                    final Runnable mEntryAnimationRunnable = new Runnable() {
651                            @Override
652                        public void run() {
653                            if (!isAdded()) {
654                                // We have been detached before this could run, so just bail.
655                                return;
656                            }
657
658                            dialogView.setVisibility(View.VISIBLE);
659
660                            // Fade in the activity background protection
661                            ObjectAnimator oa = ObjectAnimator.ofInt(bgDrawable, "alpha", 255);
662                            oa.setDuration(mAnimateInDuration);
663                            oa.setStartDelay(mSecondaryAnimateDelay);
664                            oa.setInterpolator(new DecelerateInterpolator(1.0f));
665                            oa.start();
666
667                            boolean isRtl = ViewCompat.getLayoutDirection(contentView) ==
668                                    ViewCompat.LAYOUT_DIRECTION_RTL;
669                            int startDist = isRtl ? mSlideInDistance : -mSlideInDistance;
670                            int endDist = isRtl ? -actionContainerView.getMeasuredWidth() :
671                                    actionContainerView.getMeasuredWidth();
672
673                            // Fade in and slide in the ContentFragment TextViews from the start.
674                            prepareAndAnimateView((View) contentView.getTag(R.id.title),
675                                    startDist, false);
676                            prepareAndAnimateView((View) contentView.getTag(R.id.breadcrumb),
677                                    startDist, false);
678                            prepareAndAnimateView((View) contentView.getTag(R.id.description),
679                                    startDist, false);
680
681                            // Fade in and slide in the ActionFragment from the end.
682                            prepareAndAnimateView(actionContainerView,
683                                    endDist, false);
684                            prepareAndAnimateView((View) contentView.getTag(R.id.icon),
685                                    startDist, true);
686                        }
687                    };
688                });
689    }
690
691    private void prepareAndAnimateView(final View v, float initTransX,
692            final boolean notifyAnimationFinished) {
693        v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
694        v.buildLayer();
695        v.setAlpha(0);
696        v.setTranslationX(initTransX);
697        v.animate().alpha(1f).translationX(0).setDuration(mAnimateInDuration)
698                .setStartDelay(mSecondaryAnimateDelay);
699        v.animate().setInterpolator(new DecelerateInterpolator(1.0f));
700        v.animate().setListener(new AnimatorListenerAdapter() {
701            @Override
702            public void onAnimationEnd(Animator animation) {
703                v.setLayerType(View.LAYER_TYPE_NONE, null);
704                if (notifyAnimationFinished) {
705                    onIntroAnimationFinished();
706                }
707            }
708        });
709        v.animate().start();
710    }
711
712    private Animator createDummyAnimator(final View v, ArrayList<Animator> animators) {
713        final AnimatorSet animatorSet = new AnimatorSet();
714        animatorSet.playTogether(animators);
715        return new UntargetableAnimatorSet(animatorSet);
716    }
717
718    private Animator createAnimator(View v, int resourceId) {
719        Animator animator = AnimatorInflater.loadAnimator(v.getContext(), resourceId);
720        animator.setTarget(v);
721        return animator;
722    }
723
724    private Animator createSlideOutToStartAnimator(View v) {
725        boolean isRtl = ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_RTL;
726        float toX = isRtl ? SLIDE_OUT_ANIMATOR_RIGHT : -SLIDE_OUT_ANIMATOR_RIGHT;
727        return createTranslateAlphaAnimator(v, SLIDE_OUT_ANIMATOR_LEFT, toX,
728                SLIDE_OUT_ANIMATOR_END_ALPHA, SLIDE_OUT_ANIMATOR_START_ALPHA);
729    }
730
731    private Animator createSlideInFromEndAnimator(View v) {
732        boolean isRtl = ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_RTL;
733        float fromX = isRtl ? -SLIDE_OUT_ANIMATOR_RIGHT : SLIDE_OUT_ANIMATOR_RIGHT;
734        return createTranslateAlphaAnimator(v, fromX, SLIDE_OUT_ANIMATOR_LEFT,
735                SLIDE_OUT_ANIMATOR_START_ALPHA, SLIDE_OUT_ANIMATOR_END_ALPHA);
736    }
737
738    private Animator createSlideInFromStartAnimator(View v) {
739        boolean isRtl = ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_RTL;
740        float fromX = isRtl ? SLIDE_OUT_ANIMATOR_RIGHT : -SLIDE_OUT_ANIMATOR_RIGHT;
741        return createTranslateAlphaAnimator(v, fromX, SLIDE_OUT_ANIMATOR_LEFT,
742                SLIDE_OUT_ANIMATOR_START_ALPHA, SLIDE_OUT_ANIMATOR_END_ALPHA);
743    }
744
745    private Animator createSlideOutToEndAnimator(View v) {
746        boolean isRtl = ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_RTL;
747        float toX = isRtl ? -SLIDE_OUT_ANIMATOR_RIGHT : SLIDE_OUT_ANIMATOR_RIGHT;
748        return createTranslateAlphaAnimator(v, SLIDE_OUT_ANIMATOR_LEFT, toX,
749                SLIDE_OUT_ANIMATOR_END_ALPHA, SLIDE_OUT_ANIMATOR_START_ALPHA);
750    }
751
752    private Animator createFadeOutAnimator(View v) {
753        return createAlphaAnimator(v, SLIDE_OUT_ANIMATOR_END_ALPHA, SLIDE_OUT_ANIMATOR_START_ALPHA);
754    }
755
756    private Animator createTranslateAlphaAnimator(View v, float fromTranslateX, float toTranslateX,
757            float fromAlpha, float toAlpha) {
758        ObjectAnimator translateAnimator = ObjectAnimator.ofFloat(v, "translationX", fromTranslateX,
759                toTranslateX);
760        translateAnimator.setDuration(
761                getResources().getInteger(android.R.integer.config_longAnimTime));
762        Animator alphaAnimator = createAlphaAnimator(v, fromAlpha, toAlpha);
763        AnimatorSet animatorSet = new AnimatorSet();
764        animatorSet.play(translateAnimator).with(alphaAnimator);
765        return animatorSet;
766    }
767
768    private Animator createAlphaAnimator(View v, float fromAlpha, float toAlpha) {
769        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", fromAlpha, toAlpha);
770        alphaAnimator.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
771        return alphaAnimator;
772    }
773
774    private static class SelectorAnimator extends RecyclerView.OnScrollListener {
775
776        private final View mSelectorView;
777        private final ViewGroup mParentView;
778        private final int mAnimationDuration;
779        private volatile boolean mFadedOut = true;
780
781        SelectorAnimator(View selectorView, ViewGroup parentView) {
782            mSelectorView = selectorView;
783            mParentView = parentView;
784            mAnimationDuration = selectorView.getContext()
785                    .getResources().getInteger(R.integer.lb_dialog_animation_duration);
786        }
787
788        /**
789         * We want to fade in the selector if we've stopped scrolling on it. If we're scrolling, we
790         * want to ensure to dim the selector if we haven't already. We dim the last highlighted
791         * view so that while a user is scrolling, nothing is highlighted.
792         */
793        @Override
794        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
795            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
796                // The selector starts with a height of 0. In order to scale up from 0 we first
797                // need the set the height to 1 and scale form there.
798                int selectorHeight = mSelectorView.getHeight();
799                if (selectorHeight == 0) {
800                    LayoutParams lp = mSelectorView.getLayoutParams();
801                    lp.height = selectorHeight = mSelectorView.getContext().getResources()
802                            .getDimensionPixelSize(R.dimen.lb_action_fragment_selector_min_height);
803                    mSelectorView.setLayoutParams(lp);
804                }
805                View focusedChild = mParentView.getFocusedChild();
806                if (focusedChild != null) {
807                    float scaleY = (float) focusedChild.getHeight() / selectorHeight;
808                    ViewPropertyAnimator animation = mSelectorView.animate()
809                            .alpha(1f)
810                            .setDuration(mAnimationDuration)
811                            .setInterpolator(new DecelerateInterpolator(2f));
812                    if (mFadedOut) {
813                        // Selector is completely faded out, so we can just scale before fading in.
814                        mSelectorView.setScaleY(scaleY);
815                    } else {
816                        // Selector is not faded out, so we must animate the scale as we fade in.
817                        animation.scaleY(scaleY);
818                    }
819                    animation.start();
820                }
821            } else {
822                mSelectorView.animate()
823                        .alpha(0f)
824                        .setDuration(mAnimationDuration)
825                        .setInterpolator(new DecelerateInterpolator(2f))
826                        .start();
827            }
828        }
829    }
830
831    private static class UntargetableAnimatorSet extends Animator {
832
833        private final AnimatorSet mAnimatorSet;
834
835        UntargetableAnimatorSet(AnimatorSet animatorSet) {
836            mAnimatorSet = animatorSet;
837        }
838
839        @Override
840        public void addListener(Animator.AnimatorListener listener) {
841            mAnimatorSet.addListener(listener);
842        }
843
844        @Override
845        public void cancel() {
846            mAnimatorSet.cancel();
847        }
848
849        @Override
850        public Animator clone() {
851            return mAnimatorSet.clone();
852        }
853
854        @Override
855        public void end() {
856            mAnimatorSet.end();
857        }
858
859        @Override
860        public long getDuration() {
861            return mAnimatorSet.getDuration();
862        }
863
864        @Override
865        public ArrayList<Animator.AnimatorListener> getListeners() {
866            return mAnimatorSet.getListeners();
867        }
868
869        @Override
870        public long getStartDelay() {
871            return mAnimatorSet.getStartDelay();
872        }
873
874        @Override
875        public boolean isRunning() {
876            return mAnimatorSet.isRunning();
877        }
878
879        @Override
880        public boolean isStarted() {
881            return mAnimatorSet.isStarted();
882        }
883
884        @Override
885        public void removeAllListeners() {
886            mAnimatorSet.removeAllListeners();
887        }
888
889        @Override
890        public void removeListener(Animator.AnimatorListener listener) {
891            mAnimatorSet.removeListener(listener);
892        }
893
894        @Override
895        public Animator setDuration(long duration) {
896            return mAnimatorSet.setDuration(duration);
897        }
898
899        @Override
900        public void setInterpolator(TimeInterpolator value) {
901            mAnimatorSet.setInterpolator(value);
902        }
903
904        @Override
905        public void setStartDelay(long startDelay) {
906            mAnimatorSet.setStartDelay(startDelay);
907        }
908
909        @Override
910        public void setTarget(Object target) {
911            // ignore
912        }
913
914        @Override
915        public void setupEndValues() {
916            mAnimatorSet.setupEndValues();
917        }
918
919        @Override
920        public void setupStartValues() {
921            mAnimatorSet.setupStartValues();
922        }
923
924        @Override
925        public void start() {
926            mAnimatorSet.start();
927        }
928    }
929
930}
931