1 /*
2  * Copyright (C) 2015 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.ui.sidepanel;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorInflater;
21 import android.animation.AnimatorListenerAdapter;
22 import android.app.Activity;
23 import android.app.FragmentManager;
24 import android.app.FragmentTransaction;
25 import android.view.View;
26 import android.view.ViewTreeObserver;
27 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
28 import com.android.tv.R;
29 import com.android.tv.ui.hideable.AutoHideScheduler;
30 
31 /** Manages {@link SideFragment}s. */
32 public class SideFragmentManager implements AccessibilityStateChangeListener {
33     private static final String FIRST_BACKSTACK_RECORD_NAME = "0";
34 
35     private final Activity mActivity;
36     private final FragmentManager mFragmentManager;
37     private final Runnable mPreShowRunnable;
38     private final Runnable mPostHideRunnable;
39     private ViewTreeObserver.OnGlobalLayoutListener mShowOnGlobalLayoutListener;
40 
41     // To get the count reliably while using popBackStack(),
42     // instead of using getBackStackEntryCount() with popBackStackImmediate().
43     private int mFragmentCount;
44 
45     private final View mPanel;
46     private final Animator mShowAnimator;
47     private final Animator mHideAnimator;
48 
49     private final AutoHideScheduler mAutoHideScheduler;
50     private final long mShowDurationMillis;
51 
SideFragmentManager( Activity activity, Runnable preShowRunnable, Runnable postHideRunnable)52     public SideFragmentManager(
53             Activity activity, Runnable preShowRunnable, Runnable postHideRunnable) {
54         mActivity = activity;
55         mFragmentManager = mActivity.getFragmentManager();
56         mPreShowRunnable = preShowRunnable;
57         mPostHideRunnable = postHideRunnable;
58 
59         mPanel = mActivity.findViewById(R.id.side_panel);
60         mShowAnimator = AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_enter);
61         mShowAnimator.setTarget(mPanel);
62         mHideAnimator = AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_exit);
63         mHideAnimator.setTarget(mPanel);
64         mHideAnimator.addListener(
65                 new AnimatorListenerAdapter() {
66                     @Override
67                     public void onAnimationEnd(Animator animation) {
68                         // Animation is still in running state at this point.
69                         hideAllInternal();
70                     }
71                 });
72 
73         mShowDurationMillis =
74                 mActivity.getResources().getInteger(R.integer.side_panel_show_duration);
75         mAutoHideScheduler = new AutoHideScheduler(activity, () -> hideAll(true));
76     }
77 
getCount()78     public int getCount() {
79         return mFragmentCount;
80     }
81 
isActive()82     public boolean isActive() {
83         return mFragmentCount != 0 && !isHiding();
84     }
85 
isHiding()86     public boolean isHiding() {
87         return mHideAnimator.isStarted();
88     }
89 
90     /** Shows the given {@link SideFragment}. */
show(SideFragment sideFragment)91     public void show(SideFragment sideFragment) {
92         show(sideFragment, true);
93     }
94 
95     /** Shows the given {@link SideFragment}. */
show(SideFragment sideFragment, boolean showEnterAnimation)96     public void show(SideFragment sideFragment, boolean showEnterAnimation) {
97         if (isHiding()) {
98             mHideAnimator.end();
99         }
100         boolean isFirst = (mFragmentCount == 0);
101         FragmentTransaction ft = mFragmentManager.beginTransaction();
102         if (!isFirst) {
103             ft.setCustomAnimations(
104                     showEnterAnimation ? R.animator.side_panel_fragment_enter : 0,
105                     R.animator.side_panel_fragment_exit,
106                     R.animator.side_panel_fragment_pop_enter,
107                     R.animator.side_panel_fragment_pop_exit);
108         }
109         ft.replace(R.id.side_fragment_container, sideFragment)
110                 .addToBackStack(Integer.toString(mFragmentCount))
111                 .commit();
112         mFragmentCount++;
113 
114         if (isFirst) {
115             // We should wait for fragment transition and intital layouting finished to start the
116             // slide-in animation to prevent jankiness resulted by performing transition and
117             // layouting at the same time with animation.
118             mPanel.setVisibility(View.VISIBLE);
119             mShowOnGlobalLayoutListener =
120                     new ViewTreeObserver.OnGlobalLayoutListener() {
121                         @Override
122                         public void onGlobalLayout() {
123                             mPanel.getViewTreeObserver().removeOnGlobalLayoutListener(this);
124                             mShowOnGlobalLayoutListener = null;
125                             if (mPreShowRunnable != null) {
126                                 mPreShowRunnable.run();
127                             }
128                             mShowAnimator.start();
129                         }
130                     };
131             mPanel.getViewTreeObserver().addOnGlobalLayoutListener(mShowOnGlobalLayoutListener);
132         }
133         scheduleHideAll();
134     }
135 
popSideFragment()136     public void popSideFragment() {
137         if (!isActive()) {
138             return;
139         } else if (mFragmentCount == 1) {
140             // Show closing animation with the last fragment.
141             hideAll(true);
142             return;
143         }
144         mFragmentManager.popBackStack();
145         mFragmentCount--;
146     }
147 
hideAll(boolean withAnimation)148     public void hideAll(boolean withAnimation) {
149         if (mShowAnimator.isStarted()) {
150             mShowAnimator.end();
151         }
152         if (mShowOnGlobalLayoutListener != null) {
153             // The show operation maybe requested but the show animator is not started yet, in this
154             // case, we show still run mPreShowRunnable.
155             mPanel.getViewTreeObserver().removeOnGlobalLayoutListener(mShowOnGlobalLayoutListener);
156             mShowOnGlobalLayoutListener = null;
157             if (mPreShowRunnable != null) {
158                 mPreShowRunnable.run();
159             }
160         }
161         if (withAnimation) {
162             if (!isHiding()) {
163                 mHideAnimator.start();
164             }
165             return;
166         }
167         if (isHiding()) {
168             mHideAnimator.end();
169             return;
170         }
171         hideAllInternal();
172     }
173 
hideAllInternal()174     private void hideAllInternal() {
175         mAutoHideScheduler.cancel();
176         if (mFragmentCount == 0) {
177             return;
178         }
179 
180         mPanel.setVisibility(View.GONE);
181         mFragmentManager.popBackStack(
182                 FIRST_BACKSTACK_RECORD_NAME, FragmentManager.POP_BACK_STACK_INCLUSIVE);
183         mFragmentCount = 0;
184 
185         if (mPostHideRunnable != null) {
186             mPostHideRunnable.run();
187         }
188     }
189 
190     /**
191      * Show the side panel with animation. If there are many entries in the fragment stack, the
192      * animation look like that there's only one fragment.
193      *
194      * @param withAnimation specifies if animation should be shown.
195      */
showSidePanel(boolean withAnimation)196     public void showSidePanel(boolean withAnimation) {
197         if (mFragmentCount == 0) {
198             return;
199         }
200 
201         mPanel.setVisibility(View.VISIBLE);
202         if (withAnimation) {
203             mShowAnimator.start();
204         }
205         scheduleHideAll();
206     }
207 
208     /**
209      * Hide the side panel. This method just hide the panel and preserves the back stack. If you
210      * want to empty the back stack, call {@link #hideAll}.
211      */
hideSidePanel(boolean withAnimation)212     public void hideSidePanel(boolean withAnimation) {
213         mAutoHideScheduler.cancel();
214         if (withAnimation) {
215             Animator hideAnimator =
216                     AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_exit);
217             hideAnimator.setTarget(mPanel);
218             hideAnimator.start();
219             hideAnimator.addListener(
220                     new AnimatorListenerAdapter() {
221                         @Override
222                         public void onAnimationEnd(Animator animation) {
223                             mPanel.setVisibility(View.GONE);
224                         }
225                     });
226         } else {
227             mPanel.setVisibility(View.GONE);
228         }
229     }
230 
isSidePanelVisible()231     public boolean isSidePanelVisible() {
232         return mPanel.getVisibility() == View.VISIBLE;
233     }
234 
235     /** Resets the timer for hiding side fragment. */
scheduleHideAll()236     public void scheduleHideAll() {
237         mAutoHideScheduler.schedule(mShowDurationMillis);
238     }
239 
240     /** Should {@code keyCode} hide the current panel. */
isHideKeyForCurrentPanel(int keyCode)241     public boolean isHideKeyForCurrentPanel(int keyCode) {
242         if (isActive()) {
243             SideFragment current =
244                     (SideFragment) mFragmentManager.findFragmentById(R.id.side_fragment_container);
245             return current != null && current.isHideKeyForThisPanel(keyCode);
246         }
247         return false;
248     }
249 
250     @Override
onAccessibilityStateChanged(boolean enabled)251     public void onAccessibilityStateChanged(boolean enabled) {
252         mAutoHideScheduler.onAccessibilityStateChanged(enabled);
253     }
254 }
255