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