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