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.BitmapDrawable;
37import android.graphics.drawable.ColorDrawable;
38import android.graphics.drawable.Drawable;
39import android.net.Uri;
40import android.os.Bundle;
41import android.os.Parcel;
42import android.os.Parcelable;
43import android.support.v17.leanback.R;
44import android.support.v17.leanback.widget.VerticalGridView;
45import android.support.v7.widget.RecyclerView;
46import android.util.Log;
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 com.android.tv.settings.util.AccessibilityHelper;
60import com.android.tv.settings.widget.BitmapWorkerOptions;
61import com.android.tv.settings.widget.DrawableDownloader;
62import com.android.tv.settings.widget.DrawableDownloader.BitmapCallback;
63
64import java.util.ArrayList;
65import java.util.List;
66
67/**
68 * Displays content on the left and actions on the right.
69 */
70public class DialogFragment extends Fragment {
71
72    private static final String TAG_LEAN_BACK_DIALOG_FRAGMENT = "leanBackDialogFragment";
73    private static final String EXTRA_CONTENT_TITLE = "title";
74    private static final String EXTRA_CONTENT_BREADCRUMB = "breadcrumb";
75    private static final String EXTRA_CONTENT_DESCRIPTION = "description";
76    private static final String EXTRA_CONTENT_ICON_RESOURCE_ID = "iconResourceId";
77    private static final String EXTRA_CONTENT_ICON_URI = "iconUri";
78    private static final String EXTRA_CONTENT_ICON_BITMAP = "iconBitmap";
79    private static final String EXTRA_CONTENT_ICON_BACKGROUND = "iconBackground";
80    private static final String EXTRA_ACTION_NAME = "name";
81    private static final String EXTRA_ACTION_ACTIONS = "actions";
82    private static final String EXTRA_ACTION_SELECTED_INDEX = "selectedIndex";
83    private static final String EXTRA_ENTRY_TRANSITION_PERFORMED = "entryTransitionPerformed";
84    private static final int ANIMATE_IN_DURATION = 250;
85    private static final int ANIMATE_DELAY = 550;
86    private static final int SECONDARY_ANIMATE_DELAY = 120;
87    private static final int SLIDE_IN_STAGGER = 100;
88    private static final int SLIDE_IN_DISTANCE = 120;
89    private static final int ANIMATION_FRAGMENT_ENTER = 1;
90    private static final int ANIMATION_FRAGMENT_EXIT = 2;
91    private static final int ANIMATION_FRAGMENT_ENTER_POP = 3;
92    private static final int ANIMATION_FRAGMENT_EXIT_POP = 4;
93
94    /**
95     * Builds a LeanBackDialogFragment object.
96     */
97    public static class Builder {
98
99        private String mContentTitle;
100        private String mContentBreadcrumb;
101        private String mContentDescription;
102        private int mIconResourceId;
103        private Uri mIconUri;
104        private Bitmap mIconBitmap;
105        private int mIconBackgroundColor = Color.TRANSPARENT;
106        private ArrayList<Action> mActions;
107        private String mName;
108        private int mSelectedIndex;
109
110        public DialogFragment build() {
111            DialogFragment fragment = new DialogFragment();
112            Bundle args = new Bundle();
113            args.putString(EXTRA_CONTENT_TITLE, mContentTitle);
114            args.putString(EXTRA_CONTENT_BREADCRUMB, mContentBreadcrumb);
115            args.putString(EXTRA_CONTENT_DESCRIPTION, mContentDescription);
116            args.putInt(EXTRA_CONTENT_ICON_RESOURCE_ID, mIconResourceId);
117            args.putParcelable(EXTRA_CONTENT_ICON_URI, mIconUri);
118            args.putParcelable(EXTRA_CONTENT_ICON_BITMAP, mIconBitmap);
119            args.putInt(EXTRA_CONTENT_ICON_BACKGROUND, mIconBackgroundColor);
120            args.putParcelableArrayList(EXTRA_ACTION_ACTIONS, mActions);
121            args.putString(EXTRA_ACTION_NAME, mName);
122            args.putInt(EXTRA_ACTION_SELECTED_INDEX, mSelectedIndex);
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 iconResourceId(int iconResourceId) {
143            mIconResourceId = iconResourceId;
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 actions(ArrayList<Action> actions) {
163            mActions = actions;
164            return this;
165        }
166
167        public Builder name(String name) {
168            mName = name;
169            return this;
170        }
171
172        public Builder selectedIndex(int selectedIndex) {
173            mSelectedIndex = selectedIndex;
174            return this;
175        }
176    }
177
178    public static void add(FragmentManager fm, DialogFragment f) {
179        boolean hasDialog = fm.findFragmentByTag(TAG_LEAN_BACK_DIALOG_FRAGMENT) != null;
180        FragmentTransaction ft = fm.beginTransaction();
181
182        if (hasDialog) {
183            ft.setCustomAnimations(ANIMATION_FRAGMENT_ENTER,
184                    ANIMATION_FRAGMENT_EXIT, ANIMATION_FRAGMENT_ENTER_POP,
185                    ANIMATION_FRAGMENT_EXIT_POP);
186            ft.addToBackStack(null);
187        }
188        ft.replace(android.R.id.content, f, TAG_LEAN_BACK_DIALOG_FRAGMENT).commit();
189    }
190
191    private DialogActionAdapter mAdapter;
192    private SelectorAnimator mSelectorAnimator;
193    private VerticalGridView mListView;
194    private Action.Listener mListener;
195    private String mTitle;
196    private String mBreadcrumb;
197    private String mDescription;
198    private int mIconResourceId;
199    private Uri mIconUri;
200    private Bitmap mIconBitmap;
201    private int mIconBackgroundColor = Color.TRANSPARENT;
202    private ArrayList<Action> mActions;
203    private String mName;
204    private int mSelectedIndex = -1;
205    private boolean mEntryTransitionPerformed;
206    private boolean mIntroAnimationInProgress;
207    private BitmapCallback mBitmapCallBack;
208
209    @Override
210    public void onCreate(Bundle savedInstanceState) {
211        android.util.Log.v("DialogFragment", "onCreate");
212        super.onCreate(savedInstanceState);
213        Bundle state = (savedInstanceState != null) ? savedInstanceState : getArguments();
214        if (mTitle == null) {
215            mTitle = state.getString(EXTRA_CONTENT_TITLE);
216        }
217        if (mBreadcrumb == null) {
218            mBreadcrumb = state.getString(EXTRA_CONTENT_BREADCRUMB);
219        }
220        if (mDescription == null) {
221            mDescription = state.getString(EXTRA_CONTENT_DESCRIPTION);
222        }
223        if (mIconResourceId == 0) {
224            mIconResourceId = state.getInt(EXTRA_CONTENT_ICON_RESOURCE_ID, 0);
225        }
226        if (mIconUri == null) {
227            mIconUri = state.getParcelable(EXTRA_CONTENT_ICON_URI);
228        }
229        if (mIconBitmap == null) {
230            mIconBitmap = state.getParcelable(EXTRA_CONTENT_ICON_BITMAP);
231        }
232        if (mIconBackgroundColor == Color.TRANSPARENT) {
233            mIconBackgroundColor = state.getInt(EXTRA_CONTENT_ICON_BACKGROUND, Color.TRANSPARENT);
234        }
235        if (mActions == null) {
236            mActions = state.getParcelableArrayList(EXTRA_ACTION_ACTIONS);
237        }
238        if (mName == null) {
239            mName = state.getString(EXTRA_ACTION_NAME);
240        }
241        if (mSelectedIndex == -1) {
242            mSelectedIndex = state.getInt(EXTRA_ACTION_SELECTED_INDEX, -1);
243        }
244        mEntryTransitionPerformed = state.getBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, false);
245    }
246
247    @Override
248    public View onCreateView(LayoutInflater inflater, ViewGroup container,
249            Bundle savedInstanceState) {
250
251        View v = inflater.inflate(R.layout.lb_dialog_fragment, container, false);
252
253        View contentContainer = v.findViewById(R.id.content_fragment);
254        View content = inflater.inflate(R.layout.lb_dialog_content, container, false);
255        ((ViewGroup) contentContainer).addView(content);
256        setContentView(content);
257        v.setTag(R.id.content_fragment, content);
258
259        View actionContainer = v.findViewById(R.id.action_fragment);
260        View action = inflater.inflate(R.layout.lb_dialog_action_list, container, false);
261        ((ViewGroup) actionContainer).addView(action);
262        setActionView(action);
263        v.setTag(R.id.action_fragment, action);
264
265        return v;
266    }
267
268    @Override
269    public void onSaveInstanceState(Bundle outState) {
270        super.onSaveInstanceState(outState);
271        outState.putString(EXTRA_CONTENT_TITLE, mTitle);
272        outState.putString(EXTRA_CONTENT_BREADCRUMB, mBreadcrumb);
273        outState.putString(EXTRA_CONTENT_DESCRIPTION, mDescription);
274        outState.putInt(EXTRA_CONTENT_ICON_RESOURCE_ID, mIconResourceId);
275        outState.putParcelable(EXTRA_CONTENT_ICON_URI, mIconUri);
276        outState.putParcelable(EXTRA_CONTENT_ICON_BITMAP, mIconBitmap);
277        outState.putInt(EXTRA_CONTENT_ICON_BACKGROUND, mIconBackgroundColor);
278        outState.putParcelableArrayList(EXTRA_ACTION_ACTIONS, mActions);
279        outState.putInt(EXTRA_ACTION_SELECTED_INDEX,
280                (mListView != null) ? getSelectedItemPosition() : mSelectedIndex);
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            performSelectorTransition();
293        }
294    }
295
296    @Override
297    public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
298        View dialogView = getView();
299        View contentView = (View) dialogView.getTag(R.id.content_fragment);
300        View actionView = (View) dialogView.getTag(R.id.action_fragment);
301        View actionContainerView = dialogView.findViewById(R.id.action_fragment);
302        View titleView = (View) contentView.getTag(R.id.title);
303        View breadcrumbView = (View) contentView.getTag(R.id.breadcrumb);
304        View descriptionView = (View) contentView.getTag(R.id.description);
305        View iconView = (View) contentView.getTag(R.id.icon);
306        View listView = (View) actionView.getTag(R.id.list);
307        View selectorView = (View) actionView.getTag(R.id.selector);
308
309        ArrayList<Animator> animators = new ArrayList<Animator>();
310
311        switch (nextAnim) {
312            case ANIMATION_FRAGMENT_ENTER:
313                animators.add(createSlideLeftInAnimator(titleView));
314                animators.add(createSlideLeftInAnimator(breadcrumbView));
315                animators.add(createSlideLeftInAnimator(descriptionView));
316                animators.add(createSlideLeftInAnimator(iconView));
317                animators.add(createSlideLeftInAnimator(listView));
318                animators.add(createSlideLeftInAnimator(selectorView));
319                break;
320            case ANIMATION_FRAGMENT_EXIT:
321                animators.add(createSlideLeftOutAnimator(titleView));
322                animators.add(createSlideLeftOutAnimator(breadcrumbView));
323                animators.add(createSlideLeftOutAnimator(descriptionView));
324                animators.add(createSlideLeftOutAnimator(iconView));
325                animators.add(createSlideLeftOutAnimator(listView));
326                animators.add(createSlideLeftOutAnimator(selectorView));
327                animators.add(createFadeOutAnimator(actionContainerView));
328                break;
329            case ANIMATION_FRAGMENT_ENTER_POP:
330                animators.add(createSlideRightInAnimator(titleView));
331                animators.add(createSlideRightInAnimator(breadcrumbView));
332                animators.add(createSlideRightInAnimator(descriptionView));
333                animators.add(createSlideRightInAnimator(iconView));
334                animators.add(createSlideRightInAnimator(listView));
335                animators.add(createSlideRightInAnimator(selectorView));
336                break;
337            case ANIMATION_FRAGMENT_EXIT_POP:
338                animators.add(createSlideRightOutAnimator(titleView));
339                animators.add(createSlideRightOutAnimator(breadcrumbView));
340                animators.add(createSlideRightOutAnimator(descriptionView));
341                animators.add(createSlideRightOutAnimator(iconView));
342                animators.add(createSlideRightOutAnimator(listView));
343                animators.add(createSlideRightOutAnimator(selectorView));
344                animators.add(createFadeOutAnimator(actionContainerView));
345                break;
346            default:
347                return super.onCreateAnimator(transit, enter, nextAnim);
348        }
349
350        mEntryTransitionPerformed = true;
351        return createDummyAnimator(dialogView, animators);
352    }
353
354    public void setIcon(int iconResourceId) {
355        mIconResourceId = iconResourceId;
356        View v = getView();
357        if (v != null) {
358            final ImageView iconImageView = (ImageView) v.findViewById(R.id.icon);
359            if (iconImageView != null) {
360                if (iconResourceId != 0) {
361                    iconImageView.setImageResource(iconResourceId);
362                    iconImageView.setVisibility(View.VISIBLE);
363                    updateViewSize(iconImageView);
364                }
365            }
366        }
367    }
368
369    /**
370     * Fragments need to call this method in its {@link #onResume()} to set the
371     * custom listener. <br/>
372     * Activities do not need to call this method
373     *
374     * @param listener
375     */
376    public void setListener(Action.Listener listener) {
377        mListener = listener;
378    }
379
380    public boolean hasListener() {
381        return mListener != null;
382    }
383
384    public ArrayList<Action> getActions() {
385        return mActions;
386    }
387
388    public void setActions(ArrayList<Action> actions) {
389        mActions = actions;
390        if (mAdapter != null) {
391            mAdapter.setActions(mActions);
392        }
393    }
394
395    public View getItemView(int position) {
396        return mListView.getChildAt(position);
397    }
398
399    public void setSelectedPosition(int position) {
400        mListView.setSelectedPosition(position);
401    }
402
403    public int getSelectedItemPosition() {
404        return mListView.indexOfChild(mListView.getFocusedChild());
405    }
406
407    /**
408     * Called when intro animation is finished.
409     * <p>
410     * If a subclass is going to alter the view, should wait until this is
411     * called.
412     */
413    public void onIntroAnimationFinished() {
414        mIntroAnimationInProgress = false;
415    }
416
417    public boolean isIntroAnimationInProgress() {
418        return mIntroAnimationInProgress;
419    }
420
421    private void setContentView(View content) {
422        TextView titleView = (TextView) content.findViewById(R.id.title);
423        TextView breadcrumbView = (TextView) content.findViewById(R.id.breadcrumb);
424        TextView descriptionView = (TextView) content.findViewById(R.id.description);
425        titleView.setText(mTitle);
426        breadcrumbView.setText(mBreadcrumb);
427        descriptionView.setText(mDescription);
428        final ImageView iconImageView = (ImageView) content.findViewById(R.id.icon);
429        if (mIconBackgroundColor != Color.TRANSPARENT) {
430            iconImageView.setBackgroundColor(mIconBackgroundColor);
431        }
432
433        if (AccessibilityHelper.forceFocusableViews(getActivity())) {
434            titleView.setFocusable(true);
435            titleView.setFocusableInTouchMode(true);
436            descriptionView.setFocusable(true);
437            descriptionView.setFocusableInTouchMode(true);
438            breadcrumbView.setFocusable(true);
439            breadcrumbView.setFocusableInTouchMode(true);
440        }
441
442        if (mIconResourceId != 0) {
443            iconImageView.setImageResource(mIconResourceId);
444            updateViewSize(iconImageView);
445        } else {
446            if (mIconBitmap != null) {
447                iconImageView.setImageBitmap(mIconBitmap);
448                updateViewSize(iconImageView);
449            } else {
450                if (mIconUri != null) {
451                    iconImageView.setVisibility(View.INVISIBLE);
452
453                    DrawableDownloader bitmapDownloader = DrawableDownloader.getInstance(
454                            content.getContext());
455                    mBitmapCallBack = new BitmapCallback() {
456                        @Override
457                        public void onBitmapRetrieved(Drawable bitmap) {
458                            if (bitmap != null) {
459                                mIconBitmap = (bitmap instanceof BitmapDrawable) ? ((BitmapDrawable) bitmap)
460                                        .getBitmap()
461                                        : null;
462                                iconImageView.setVisibility(View.VISIBLE);
463                                iconImageView.setImageDrawable(bitmap);
464                                updateViewSize(iconImageView);
465                            }
466                        }
467                    };
468
469                    bitmapDownloader.getBitmap(new BitmapWorkerOptions.Builder(
470                            content.getContext()).resource(mIconUri)
471                            .width(iconImageView.getLayoutParams().width).build(),
472                            mBitmapCallBack);
473                } else {
474                    iconImageView.setVisibility(View.GONE);
475                }
476            }
477        }
478
479        content.setTag(R.id.title, titleView);
480        content.setTag(R.id.breadcrumb, breadcrumbView);
481        content.setTag(R.id.description, descriptionView);
482        content.setTag(R.id.icon, iconImageView);
483    }
484
485    private void setActionView(View action) {
486        mAdapter = new DialogActionAdapter(new Action.Listener() {
487            @Override
488            public void onActionClicked(Action action) {
489                // eat events if action is disabled or only displays info
490                if (!action.isEnabled() || action.infoOnly()) {
491                    return;
492                }
493
494                /**
495                 * If the custom lister has been set using
496                 * {@link #setListener(DialogActionAdapter.Listener)}, use it.
497                 * If not, use the activity's default listener.
498                 */
499                if (mListener != null) {
500                    mListener.onActionClicked(action);
501                } else if (getActivity() instanceof Action.Listener) {
502                    Action.Listener listener = (Action.Listener) getActivity();
503                    listener.onActionClicked(action);
504                }
505            }
506        }, new Action.OnFocusListener() {
507            @Override
508            public void onActionFocused(Action action) {
509                if (getActivity() instanceof Action.OnFocusListener) {
510                    Action.OnFocusListener listener = (Action.OnFocusListener) getActivity();
511                    listener.onActionFocused(action);
512                }
513            }
514        }, mActions);
515
516        if (action instanceof VerticalGridView) {
517            mListView = (VerticalGridView) action;
518        } else {
519            mListView = (VerticalGridView) action.findViewById(R.id.list);
520            if (mListView == null) {
521                throw new IllegalStateException("No ListView exists.");
522            }
523//            mListView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
524//            mListView.setWindowAlignmentOffsetPercent(0.5f);
525//            mListView.setItemAlignmentOffset(0);
526//            mListView.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
527            mListView.setWindowAlignmentOffset(0);
528            mListView.setWindowAlignmentOffsetPercent(50f);
529            mListView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
530            View selectorView = action.findViewById(R.id.selector);
531            if (selectorView != null) {
532                mSelectorAnimator = new SelectorAnimator(selectorView, mListView);
533                mListView.setOnScrollListener(mSelectorAnimator);
534            }
535        }
536
537        mListView.requestFocusFromTouch();
538        mListView.setAdapter(mAdapter);
539        mListView.setSelectedPosition(
540                (mSelectedIndex >= 0 && mSelectedIndex < mActions.size()) ? mSelectedIndex
541                : getFirstCheckedAction());
542
543        action.setTag(R.id.list, mListView);
544        action.setTag(R.id.selector, action.findViewById(R.id.selector));
545    }
546
547    private int getFirstCheckedAction() {
548        for (int i = 0, size = mActions.size(); i < size; i++) {
549            if (mActions.get(i).isChecked()) {
550                return i;
551            }
552        }
553        return 0;
554    }
555
556    private void updateViewSize(ImageView iconView) {
557        int intrinsicWidth = iconView.getDrawable().getIntrinsicWidth();
558        LayoutParams lp = iconView.getLayoutParams();
559        if (intrinsicWidth > 0) {
560            lp.height = lp.width * iconView.getDrawable().getIntrinsicHeight()
561                    / intrinsicWidth;
562        } else {
563            // If no intrinsic width, then just mke this a square.
564            lp.height = lp.width;
565        }
566    }
567
568    private void fadeIn(View v) {
569        v.setAlpha(0f);
570        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", 1f);
571        alphaAnimator.setDuration(v.getContext().getResources().getInteger(
572                android.R.integer.config_mediumAnimTime));
573        alphaAnimator.start();
574    }
575
576    private void runDelayedAnim(final Runnable runnable) {
577        final View dialogView = getView();
578        final View contentView = (View) dialogView.getTag(R.id.content_fragment);
579
580        contentView.getViewTreeObserver().addOnGlobalLayoutListener(
581                new ViewTreeObserver.OnGlobalLayoutListener() {
582                @Override
583                    public void onGlobalLayout() {
584                        contentView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
585                        // if we buildLayer() at this time, the texture is
586                        // actually not created delay a little so we can make
587                        // sure all hardware layer is created before animation,
588                        // in that way we can avoid the jittering of start
589                        // animation
590                        contentView.postOnAnimationDelayed(runnable, ANIMATE_DELAY);
591                    }
592                });
593
594    }
595
596    private void performSelectorTransition() {
597        runDelayedAnim(new Runnable() {
598            @Override
599            public void run() {
600                // Fade in the selector.
601                if (mSelectorAnimator != null) {
602                    mSelectorAnimator.fadeIn();
603                }
604            }
605        });
606    }
607
608    private void performEntryTransition() {
609        final View dialogView = getView();
610        final View contentView = (View) dialogView.getTag(R.id.content_fragment);
611        final View actionContainerView = dialogView.findViewById(R.id.action_fragment);
612
613        mIntroAnimationInProgress = true;
614
615        // Fade out the old activity.
616        getActivity().overridePendingTransition(0, R.anim.lb_dialog_fade_out);
617
618        int bgColor = contentView.getContext().getResources()
619                .getColor(R.color.lb_dialog_activity_background);
620        final ColorDrawable bgDrawable = new ColorDrawable();
621        bgDrawable.setColor(bgColor);
622        bgDrawable.setAlpha(0);
623        dialogView.setBackground(bgDrawable);
624        dialogView.setVisibility(View.INVISIBLE);
625
626        runDelayedAnim(new Runnable() {
627            @Override
628            public void run() {
629                if (!isAdded()) {
630                    // We have been detached before this could run,
631                    // so just bail
632                    return;
633                }
634
635                dialogView.setVisibility(View.VISIBLE);
636
637                // Fade in the activity background protection
638                ObjectAnimator oa = ObjectAnimator.ofInt(bgDrawable, "alpha", 255);
639                oa.setDuration(ANIMATE_IN_DURATION);
640                oa.setStartDelay(SECONDARY_ANIMATE_DELAY);
641                oa.setInterpolator(new DecelerateInterpolator(1.0f));
642                oa.start();
643
644                // Fade in and slide in the ContentFragment
645                // TextViews from the left.
646                prepareAndAnimateView((View) contentView.getTag(R.id.title),
647                        -SLIDE_IN_DISTANCE, false);
648                prepareAndAnimateView((View) contentView.getTag(R.id.breadcrumb),
649                        -SLIDE_IN_DISTANCE, false);
650                prepareAndAnimateView((View) contentView.getTag(R.id.description),
651                        -SLIDE_IN_DISTANCE, false);
652
653                // Fade in and slide in the ActionFragment from the
654                // right.
655                prepareAndAnimateView(actionContainerView,
656                        actionContainerView.getMeasuredWidth(), false);
657                prepareAndAnimateView((View) contentView.getTag(R.id.icon),
658                        -SLIDE_IN_DISTANCE, true);
659
660                // Fade in the selector.
661                if (mSelectorAnimator != null) {
662                    mSelectorAnimator.fadeIn();
663                }
664            }
665        });
666    }
667
668    private void prepareAndAnimateView(final View v, float initTransX,
669            final boolean notifyAnimationFinished) {
670        v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
671        v.buildLayer();
672        v.setAlpha(0);
673        v.setTranslationX(initTransX);
674        v.animate().alpha(1f).translationX(0).setDuration(ANIMATE_IN_DURATION)
675                .setStartDelay(SECONDARY_ANIMATE_DELAY);
676        v.animate().setInterpolator(new DecelerateInterpolator(1.0f));
677        v.animate().setListener(new AnimatorListenerAdapter() {
678                @Override
679            public void onAnimationEnd(Animator animation) {
680                v.setLayerType(View.LAYER_TYPE_NONE, null);
681                if (notifyAnimationFinished) {
682                    onIntroAnimationFinished();
683                }
684            }
685        });
686        v.animate().start();
687    }
688
689    private Animator createDummyAnimator(final View v, ArrayList<Animator> animators) {
690        final AnimatorSet animatorSet = new AnimatorSet();
691        animatorSet.playTogether(animators);
692        return new UntargetableAnimatorSet(animatorSet);
693    }
694
695    private Animator createAnimator(View v, int resourceId) {
696        Animator animator = AnimatorInflater.loadAnimator(v.getContext(), resourceId);
697        animator.setTarget(v);
698        return animator;
699    }
700
701    private Animator createSlideLeftOutAnimator(View v) {
702        return createTranslateAlphaAnimator(v, 0, -200f, 1f, 0);
703    }
704
705    private Animator createSlideLeftInAnimator(View v) {
706        return createTranslateAlphaAnimator(v, 200f, 0, 0, 1f);
707    }
708
709    private Animator createSlideRightInAnimator(View v) {
710        return createTranslateAlphaAnimator(v, -200f, 0, 0, 1f);
711    }
712
713    private Animator createSlideRightOutAnimator(View v) {
714        return createTranslateAlphaAnimator(v, 0, 200f, 1f, 0);
715    }
716
717    private Animator createFadeOutAnimator(View v) {
718        return createAlphaAnimator(v, 1f, 0);
719    }
720
721    private Animator createTranslateAlphaAnimator(View v, float fromTranslateX, float toTranslateX,
722            float fromAlpha, float toAlpha) {
723        ObjectAnimator translateAnimator = ObjectAnimator.ofFloat(v, "translationX", fromTranslateX,
724                toTranslateX);
725        translateAnimator.setDuration(
726                getResources().getInteger(android.R.integer.config_longAnimTime));
727        Animator alphaAnimator = createAlphaAnimator(v, fromAlpha, toAlpha);
728        AnimatorSet animatorSet = new AnimatorSet();
729        animatorSet.play(translateAnimator).with(alphaAnimator);
730        return animatorSet;
731    }
732
733    private Animator createAlphaAnimator(View v, float fromAlpha, float toAlpha) {
734        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", fromAlpha, toAlpha);
735        alphaAnimator.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
736        return alphaAnimator;
737    }
738
739    private static class SelectorAnimator extends RecyclerView.OnScrollListener {
740
741        private final View mSelectorView;
742        private final ViewGroup mParentView;
743        private final int mAnimationDuration;
744        private volatile boolean mFadedOut = true;
745
746        SelectorAnimator(View selectorView, ViewGroup parentView) {
747            mSelectorView = selectorView;
748            mParentView = parentView;
749            mAnimationDuration = selectorView.getContext()
750                    .getResources().getInteger(R.integer.lb_dialog_animation_duration);
751        }
752
753        // We want to fade in the selector if we've stopped scrolling on it. If
754        // we're scrolling, we want to ensure to dim the selector if we haven't
755        // already. We dim the last highlighted view so that while a user is
756        // scrolling, nothing is highlighted.
757        @Override
758        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
759            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
760                fadeIn();
761            } else {
762                fadeOut();
763            }
764        }
765
766        public void fadeIn() {
767            // The selector starts with a height of 0. In order to scale up
768            // from
769            // 0 we first need the set the height to 1 and scale form there.
770            int selectorHeight = mSelectorView.getHeight();
771            if (selectorHeight == 0) {
772                LayoutParams lp = mSelectorView.getLayoutParams();
773                lp.height = selectorHeight = mSelectorView.getContext().getResources()
774                        .getDimensionPixelSize(R.dimen.lb_action_fragment_selector_min_height);
775                mSelectorView.setLayoutParams(lp);
776            }
777            View focusedChild = mParentView.getFocusedChild();
778            if (focusedChild != null) {
779                float scaleY = (float) focusedChild.getHeight() / selectorHeight;
780                ViewPropertyAnimator animation = mSelectorView.animate()
781                        .alpha(1f)
782                        .setListener(new Listener(false))
783                        .setDuration(mAnimationDuration)
784                        .setInterpolator(new DecelerateInterpolator(2f));
785                if (mFadedOut) {
786                    // selector is completely faded out, so we can just
787                    // scale
788                    // before fading in.
789                    mSelectorView.setScaleY(scaleY);
790                } else {
791                    // selector is not faded out, so we must animate the
792                    // scale
793                    // as we fade in.
794                    animation.scaleY(scaleY);
795                }
796                animation.start();
797            }
798        }
799
800        public void fadeOut() {
801            mSelectorView.animate()
802            .alpha(0f)
803            .setDuration(mAnimationDuration)
804            .setInterpolator(new DecelerateInterpolator(2f))
805            .setListener(new Listener(true))
806            .start();
807        }
808
809        /**
810         * Sets {@link BaseScrollAdapterFragment#mFadedOut}
811         * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff
812         * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0
813         * (faded out). If false the view either has an alpha of 1 (visible) or
814         * is in the process of animating.
815         */
816        private class Listener implements Animator.AnimatorListener {
817            private boolean mFadingOut;
818            private boolean mCanceled;
819
820            public Listener(boolean fadingOut) {
821                mFadingOut = fadingOut;
822            }
823
824            @Override
825            public void onAnimationStart(Animator animation) {
826                if (!mFadingOut) {
827                    mFadedOut = false;
828                }
829            }
830
831            @Override
832            public void onAnimationEnd(Animator animation) {
833                if (!mCanceled && mFadingOut) {
834                    mFadedOut = true;
835                }
836            }
837
838            @Override
839            public void onAnimationCancel(Animator animation) {
840                mCanceled = true;
841            }
842
843            @Override
844            public void onAnimationRepeat(Animator animation) {
845            }
846        }
847    }
848
849    private static class UntargetableAnimatorSet extends Animator {
850
851        private final AnimatorSet mAnimatorSet;
852
853        UntargetableAnimatorSet(AnimatorSet animatorSet) {
854            mAnimatorSet = animatorSet;
855        }
856
857        @Override
858        public void addListener(Animator.AnimatorListener listener) {
859            mAnimatorSet.addListener(listener);
860        }
861
862        @Override
863        public void cancel() {
864            mAnimatorSet.cancel();
865        }
866
867        @Override
868        public Animator clone() {
869            return mAnimatorSet.clone();
870        }
871
872        @Override
873        public void end() {
874            mAnimatorSet.end();
875        }
876
877        @Override
878        public long getDuration() {
879            return mAnimatorSet.getDuration();
880        }
881
882        @Override
883        public ArrayList<Animator.AnimatorListener> getListeners() {
884            return mAnimatorSet.getListeners();
885        }
886
887        @Override
888        public long getStartDelay() {
889            return mAnimatorSet.getStartDelay();
890        }
891
892        @Override
893        public boolean isRunning() {
894            return mAnimatorSet.isRunning();
895        }
896
897        @Override
898        public boolean isStarted() {
899            return mAnimatorSet.isStarted();
900        }
901
902        @Override
903        public void removeAllListeners() {
904            mAnimatorSet.removeAllListeners();
905        }
906
907        @Override
908        public void removeListener(Animator.AnimatorListener listener) {
909            mAnimatorSet.removeListener(listener);
910        }
911
912        @Override
913        public Animator setDuration(long duration) {
914            return mAnimatorSet.setDuration(duration);
915        }
916
917        @Override
918        public void setInterpolator(TimeInterpolator value) {
919            mAnimatorSet.setInterpolator(value);
920        }
921
922        @Override
923        public void setStartDelay(long startDelay) {
924            mAnimatorSet.setStartDelay(startDelay);
925        }
926
927        @Override
928        public void setTarget(Object target) {
929            // ignore
930        }
931
932        @Override
933        public void setupEndValues() {
934            mAnimatorSet.setupEndValues();
935        }
936
937        @Override
938        public void setupStartValues() {
939            mAnimatorSet.setupStartValues();
940        }
941
942        @Override
943        public void start() {
944            mAnimatorSet.start();
945        }
946    }
947
948    /**
949     * An data class which represents an action within an
950     * {@link DialogFragment}. A list of Actions represent a list of choices the
951     * user can make, a radio-button list of configuration options, or just a
952     * list of information.
953     */
954    public static class Action implements Parcelable {
955
956        private static final String TAG = "Action";
957
958        public static final int NO_DRAWABLE = 0;
959        public static final int NO_CHECK_SET = 0;
960        public static final int DEFAULT_CHECK_SET_ID = 1;
961
962        /**
963         * Object listening for adapter events.
964         */
965        public interface Listener {
966
967            /**
968             * Called when the user clicks on an action.
969             */
970            public void onActionClicked(Action action);
971        }
972
973        public interface OnFocusListener {
974
975            /**
976             * Called when the user focuses on an action.
977             */
978            public void onActionFocused(Action action);
979        }
980
981        /**
982         * Builds a Action object.
983         */
984        public static class Builder {
985            private String mKey;
986            private String mTitle;
987            private String mDescription;
988            private Intent mIntent;
989            private String mResourcePackageName;
990            private int mDrawableResource = NO_DRAWABLE;
991            private Uri mIconUri;
992            private boolean mChecked;
993            private boolean mMultilineDescription;
994            private boolean mHasNext;
995            private boolean mInfoOnly;
996            private int mCheckSetId = NO_CHECK_SET;
997            private boolean mEnabled = true;
998
999            public Action build() {
1000                Action action = new Action();
1001                action.mKey = mKey;
1002                action.mTitle = mTitle;
1003                action.mDescription = mDescription;
1004                action.mIntent = mIntent;
1005                action.mResourcePackageName = mResourcePackageName;
1006                action.mDrawableResource = mDrawableResource;
1007                action.mIconUri = mIconUri;
1008                action.mChecked = mChecked;
1009                action.mMultilineDescription = mMultilineDescription;
1010                action.mHasNext = mHasNext;
1011                action.mInfoOnly = mInfoOnly;
1012                action.mCheckSetId = mCheckSetId;
1013                action.mEnabled = mEnabled;
1014                return action;
1015            }
1016
1017            public Builder key(String key) {
1018                mKey = key;
1019                return this;
1020            }
1021
1022            public Builder title(String title) {
1023                mTitle = title;
1024                return this;
1025            }
1026
1027            public Builder description(String description) {
1028                mDescription = description;
1029                return this;
1030            }
1031
1032            public Builder intent(Intent intent) {
1033                mIntent = intent;
1034                return this;
1035            }
1036
1037            public Builder resourcePackageName(String resourcePackageName) {
1038                mResourcePackageName = resourcePackageName;
1039                return this;
1040            }
1041
1042            public Builder drawableResource(int drawableResource) {
1043                mDrawableResource = drawableResource;
1044                return this;
1045            }
1046
1047            public Builder iconUri(Uri iconUri) {
1048                mIconUri = iconUri;
1049                return this;
1050            }
1051
1052            public Builder checked(boolean checked) {
1053                mChecked = checked;
1054                return this;
1055            }
1056
1057            public Builder multilineDescription(boolean multilineDescription) {
1058                mMultilineDescription = multilineDescription;
1059                return this;
1060            }
1061
1062            public Builder hasNext(boolean hasNext) {
1063                mHasNext = hasNext;
1064                return this;
1065            }
1066
1067            public Builder infoOnly(boolean infoOnly) {
1068                mInfoOnly = infoOnly;
1069                return this;
1070            }
1071
1072            public Builder checkSetId(int checkSetId) {
1073                mCheckSetId = checkSetId;
1074                return this;
1075            }
1076
1077            public Builder enabled(boolean enabled) {
1078                mEnabled = enabled;
1079                return this;
1080            }
1081        }
1082
1083        private String mKey;
1084        private String mTitle;
1085        private String mDescription;
1086        private Intent mIntent;
1087
1088        /**
1089         * If not {@code null}, the package name to use to retrieve
1090         * {@link #mDrawableResource}.
1091         */
1092        private String mResourcePackageName;
1093
1094        private int mDrawableResource;
1095        private Uri mIconUri;
1096        private boolean mChecked;
1097        private boolean mMultilineDescription;
1098        private boolean mHasNext;
1099        private boolean mInfoOnly;
1100        private int mCheckSetId;
1101        private boolean mEnabled;
1102
1103        private Action() {
1104        }
1105
1106        public String getKey() {
1107            return mKey;
1108        }
1109
1110        public String getTitle() {
1111            return mTitle;
1112        }
1113
1114        public String getDescription() {
1115            return mDescription;
1116        }
1117
1118        public void setDescription(String description) {
1119            mDescription = description;
1120        }
1121
1122        public Intent getIntent() {
1123            return mIntent;
1124        }
1125
1126        public boolean isChecked() {
1127            return mChecked;
1128        }
1129
1130        public int getDrawableResource() {
1131            return mDrawableResource;
1132        }
1133
1134        public Uri getIconUri() {
1135            return mIconUri;
1136        }
1137
1138        public String getResourcePackageName() {
1139            return mResourcePackageName;
1140        }
1141
1142        /**
1143         * Returns the check set id this action is a part of. All actions in the
1144         * same list with the same check set id are considered linked. When one
1145         * of the actions within that set is selected that action becomes
1146         * checked while all the other actions become unchecked.
1147         *
1148         * @return an integer representing the check set this action is a part
1149         *         of or {@link NO_CHECK_SET} if this action isn't a
1150         *         part of a check set.
1151         */
1152        public int getCheckSetId() {
1153            return mCheckSetId;
1154        }
1155
1156        public boolean hasMultilineDescription() {
1157            return mMultilineDescription;
1158        }
1159
1160        public boolean isEnabled() {
1161            return mEnabled;
1162        }
1163
1164        public void setChecked(boolean checked) {
1165            mChecked = checked;
1166        }
1167
1168        public void setEnabled(boolean enabled) {
1169            mEnabled = enabled;
1170        }
1171
1172        /**
1173         * @return true if the action will request further user input when
1174         *         selected (such as showing another dialog or launching a new
1175         *         activity). False, otherwise.
1176         */
1177        public boolean hasNext() {
1178            return mHasNext;
1179        }
1180
1181        /**
1182         * @return true if the action will only display information and is thus
1183         *         unactionable. If both this and {@link #hasNext()} are true,
1184         *         infoOnly takes precedence. (default is false) e.g. the amount
1185         *         of storage a document uses or cost of an app.
1186         */
1187        public boolean infoOnly() {
1188            return mInfoOnly;
1189        }
1190
1191        /**
1192         * Returns an indicator to be drawn. If null is returned, no space for
1193         * the indicator will be made.
1194         *
1195         * @param context the context of the Activity this Action belongs to
1196         * @return an indicator to draw or null if no indicator space should
1197         *         exist.
1198         */
1199        public Drawable getIndicator(Context context) {
1200            if (mDrawableResource == NO_DRAWABLE) {
1201                return null;
1202            }
1203            if (mResourcePackageName == null) {
1204                return context.getResources().getDrawable(mDrawableResource);
1205            }
1206            // If we get to here, need to load the resources.
1207            Drawable icon = null;
1208            try {
1209                Context packageContext = context.createPackageContext(mResourcePackageName, 0);
1210                icon = packageContext.getResources().getDrawable(mDrawableResource);
1211            } catch (PackageManager.NameNotFoundException e) {
1212                if (Log.isLoggable(TAG, Log.WARN)) {
1213                    Log.w(TAG, "No icon for this action.");
1214                }
1215            } catch (Resources.NotFoundException e) {
1216                if (Log.isLoggable(TAG, Log.WARN)) {
1217                    Log.w(TAG, "No icon for this action.");
1218                }
1219            }
1220            return icon;
1221        }
1222
1223        public static Parcelable.Creator<Action> CREATOR = new Parcelable.Creator<Action>() {
1224
1225                @Override
1226            public Action createFromParcel(Parcel source) {
1227
1228                return new Action.Builder()
1229                        .key(source.readString())
1230                        .title(source.readString())
1231                        .description(source.readString())
1232                        .intent((Intent) source.readParcelable(Intent.class.getClassLoader()))
1233                        .resourcePackageName(source.readString())
1234                        .drawableResource(source.readInt())
1235                        .iconUri((Uri) source.readParcelable(Uri.class.getClassLoader()))
1236                        .checked(source.readInt() != 0)
1237                        .multilineDescription(source.readInt() != 0)
1238                        .checkSetId(source.readInt())
1239                        .build();
1240            }
1241
1242                @Override
1243            public Action[] newArray(int size) {
1244                return new Action[size];
1245            }
1246        };
1247
1248        @Override
1249        public int describeContents() {
1250            return 0;
1251        }
1252
1253        @Override
1254        public void writeToParcel(Parcel dest, int flags) {
1255            dest.writeString(mKey);
1256            dest.writeString(mTitle);
1257            dest.writeString(mDescription);
1258            dest.writeParcelable(mIntent, flags);
1259            dest.writeString(mResourcePackageName);
1260            dest.writeInt(mDrawableResource);
1261            dest.writeParcelable(mIconUri, flags);
1262            dest.writeInt(mChecked ? 1 : 0);
1263            dest.writeInt(mMultilineDescription ? 1 : 0);
1264            dest.writeInt(mCheckSetId);
1265        }
1266    }
1267}
1268