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 
17 package com.android.tv.settings.dialog;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorInflater;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.AnimatorSet;
23 import android.animation.ObjectAnimator;
24 import android.animation.TimeInterpolator;
25 import android.animation.ValueAnimator;
26 import android.app.Activity;
27 import android.app.Fragment;
28 import android.app.FragmentManager;
29 import android.app.FragmentTransaction;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.pm.PackageManager;
33 import android.content.res.Resources;
34 import android.graphics.Bitmap;
35 import android.graphics.Color;
36 import android.graphics.drawable.ColorDrawable;
37 import android.graphics.drawable.Drawable;
38 import android.net.Uri;
39 import android.os.Bundle;
40 import android.os.Handler;
41 import android.os.Parcel;
42 import android.os.Parcelable;
43 import android.support.v17.leanback.R;
44 import android.support.v17.leanback.widget.VerticalGridView;
45 import android.support.v4.view.ViewCompat;
46 import android.support.v7.widget.RecyclerView;
47 import android.view.LayoutInflater;
48 import android.view.View;
49 import android.view.ViewGroup;
50 import android.view.ViewPropertyAnimator;
51 import android.view.ViewTreeObserver;
52 import android.view.ViewGroup.LayoutParams;
53 import android.view.animation.DecelerateInterpolator;
54 import android.view.animation.Interpolator;
55 import android.widget.ImageView;
56 import android.widget.RelativeLayout;
57 import android.widget.TextView;
58 
59 import java.util.ArrayList;
60 import java.util.List;
61 
62 import com.android.tv.settings.dialog.Layout;
63 import com.android.tv.settings.util.AccessibilityHelper;
64 
65 /**
66  * Displays content on the left and actions on the right.
67  */
68 public 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 {
onActionClicked(Layout.Action action)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 
build()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 
title(String title)127         public Builder title(String title) {
128             mContentTitle = title;
129             return this;
130         }
131 
breadcrumb(String breadcrumb)132         public Builder breadcrumb(String breadcrumb) {
133             mContentBreadcrumb = breadcrumb;
134             return this;
135         }
136 
description(String description)137         public Builder description(String description) {
138             mContentDescription = description;
139             return this;
140         }
141 
icon(Drawable icon)142         public Builder icon(Drawable icon) {
143             mIcon = icon;
144             return this;
145         }
146 
iconUri(Uri iconUri)147         public Builder iconUri(Uri iconUri) {
148             mIconUri = iconUri;
149             return this;
150         }
151 
iconBitmap(Bitmap iconBitmap)152         public Builder iconBitmap(Bitmap iconBitmap) {
153             mIconBitmap = iconBitmap;
154             return this;
155         }
156 
iconBackgroundColor(int iconBackgroundColor)157         public Builder iconBackgroundColor(int iconBackgroundColor) {
158             mIconBackgroundColor = iconBackgroundColor;
159             return this;
160         }
161 
name(String name)162         public Builder name(String name) {
163             mName = name;
164             return this;
165         }
166     }
167 
add(FragmentManager fm, SettingsLayoutFragment f)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
onCreate(Bundle savedInstanceState)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onSaveInstanceState(Bundle outState)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
onStart()297     public void onStart() {
298         super.onStart();
299         if (!mEntryTransitionPerformed) {
300             mEntryTransitionPerformed = true;
301             performEntryTransition();
302         }
303     }
304 
setLayout(Layout layout)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.
updateViews()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 
setIcon(int resId)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
onRefreshView()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
getSelectedNode()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      */
onRowViewClicked(Layout.LayoutRow layoutRow)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      */
onBackPressed()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      */
goBackToTitle(String title)404     protected void goBackToTitle(String title) {
405         mLayout.goToTitle(title);
406         updateViews();
407     }
408 
409     @Override
onCreateAnimator(int transit, boolean enter, int nextAnim)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      */
onIntroAnimationFinished()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 
isIntroAnimationInProgress()489     public boolean isIntroAnimationInProgress() {
490         return mIntroAnimationInProgress;
491     }
492 
initializeContentView(View content)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 
setActionView(View action)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 
updateViewSize(ImageView iconView)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 
fadeIn(View v)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 
performEntryTransition()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 
prepareAndAnimateView(final View v, float initTransX, final boolean notifyAnimationFinished)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 
createDummyAnimator(final View v, ArrayList<Animator> animators)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 
createAnimator(View v, int resourceId)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 
createSlideOutToStartAnimator(View v)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 
createSlideInFromEndAnimator(View v)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 
createSlideInFromStartAnimator(View v)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 
createSlideOutToEndAnimator(View v)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 
createFadeOutAnimator(View v)746     private Animator createFadeOutAnimator(View v) {
747         return createAlphaAnimator(v, SLIDE_OUT_ANIMATOR_END_ALPHA, SLIDE_OUT_ANIMATOR_START_ALPHA);
748     }
749 
createTranslateAlphaAnimator(View v, float fromTranslateX, float toTranslateX, float fromAlpha, float toAlpha)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 
createAlphaAnimator(View v, float fromAlpha, float toAlpha)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 
SelectorAnimator(View selectorView, ViewGroup parentView)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
onScrollStateChanged(RecyclerView recyclerView, int newState)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 
UntargetableAnimatorSet(AnimatorSet animatorSet)829         UntargetableAnimatorSet(AnimatorSet animatorSet) {
830             mAnimatorSet = animatorSet;
831         }
832 
833         @Override
addListener(Animator.AnimatorListener listener)834         public void addListener(Animator.AnimatorListener listener) {
835             mAnimatorSet.addListener(listener);
836         }
837 
838         @Override
cancel()839         public void cancel() {
840             mAnimatorSet.cancel();
841         }
842 
843         @Override
clone()844         public Animator clone() {
845             return mAnimatorSet.clone();
846         }
847 
848         @Override
end()849         public void end() {
850             mAnimatorSet.end();
851         }
852 
853         @Override
getDuration()854         public long getDuration() {
855             return mAnimatorSet.getDuration();
856         }
857 
858         @Override
getListeners()859         public ArrayList<Animator.AnimatorListener> getListeners() {
860             return mAnimatorSet.getListeners();
861         }
862 
863         @Override
getStartDelay()864         public long getStartDelay() {
865             return mAnimatorSet.getStartDelay();
866         }
867 
868         @Override
isRunning()869         public boolean isRunning() {
870             return mAnimatorSet.isRunning();
871         }
872 
873         @Override
isStarted()874         public boolean isStarted() {
875             return mAnimatorSet.isStarted();
876         }
877 
878         @Override
removeAllListeners()879         public void removeAllListeners() {
880             mAnimatorSet.removeAllListeners();
881         }
882 
883         @Override
removeListener(Animator.AnimatorListener listener)884         public void removeListener(Animator.AnimatorListener listener) {
885             mAnimatorSet.removeListener(listener);
886         }
887 
888         @Override
setDuration(long duration)889         public Animator setDuration(long duration) {
890             return mAnimatorSet.setDuration(duration);
891         }
892 
893         @Override
setInterpolator(TimeInterpolator value)894         public void setInterpolator(TimeInterpolator value) {
895             mAnimatorSet.setInterpolator(value);
896         }
897 
898         @Override
setStartDelay(long startDelay)899         public void setStartDelay(long startDelay) {
900             mAnimatorSet.setStartDelay(startDelay);
901         }
902 
903         @Override
setTarget(Object target)904         public void setTarget(Object target) {
905             // ignore
906         }
907 
908         @Override
setupEndValues()909         public void setupEndValues() {
910             mAnimatorSet.setupEndValues();
911         }
912 
913         @Override
setupStartValues()914         public void setupStartValues() {
915             mAnimatorSet.setupStartValues();
916         }
917 
918         @Override
start()919         public void start() {
920             mAnimatorSet.start();
921         }
922     }
923 
924 }
925