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