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