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