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