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
17package com.android.tv.ui.sidepanel;
18
19import android.animation.Animator;
20import android.animation.AnimatorInflater;
21import android.animation.AnimatorListenerAdapter;
22import android.app.Activity;
23import android.app.FragmentManager;
24import android.app.FragmentTransaction;
25import android.os.Handler;
26import android.view.View;
27
28import com.android.tv.R;
29
30public 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
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
79    public int getCount() {
80        return mFragmentCount;
81    }
82
83    public boolean isActive() {
84        return mFragmentCount != 0 && !isHiding();
85    }
86
87    public boolean isHiding() {
88        return mHideAnimator.isStarted();
89    }
90
91    /**
92     * Shows the given {@link SideFragment}.
93     */
94    public void show(SideFragment sideFragment) {
95        show(sideFragment, true);
96    }
97
98    /**
99     * Shows the given {@link SideFragment}.
100     */
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
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
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
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     */
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     */
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
215    public boolean isSidePanelVisible() {
216        return mPanel.getVisibility() == View.VISIBLE;
217    }
218
219    /**
220     * Resets the timer for hiding side fragment.
221     */
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     */
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