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