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