blob: 5bba4097768d0a99881d1aee62c8377608dd8ab1 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.ui.sidepanel;
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import com.android.tv.R;
import com.android.tv.ui.hideable.AutoHideScheduler;
/** Manages {@link SideFragment}s. */
public class SideFragmentManager implements AccessibilityStateChangeListener {
private static final String FIRST_BACKSTACK_RECORD_NAME = "0";
private final Activity mActivity;
private final FragmentManager mFragmentManager;
private final Runnable mPreShowRunnable;
private final Runnable mPostHideRunnable;
private ViewTreeObserver.OnGlobalLayoutListener mShowOnGlobalLayoutListener;
// To get the count reliably while using popBackStack(),
// instead of using getBackStackEntryCount() with popBackStackImmediate().
private int mFragmentCount;
private final View mPanel;
private final Animator mShowAnimator;
private final Animator mHideAnimator;
private final AutoHideScheduler mAutoHideScheduler;
private final long mShowDurationMillis;
public SideFragmentManager(
Activity activity, Runnable preShowRunnable, Runnable postHideRunnable) {
mActivity = activity;
mFragmentManager = mActivity.getFragmentManager();
mPreShowRunnable = preShowRunnable;
mPostHideRunnable = postHideRunnable;
mPanel = mActivity.findViewById(R.id.side_panel);
mShowAnimator = AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_enter);
mShowAnimator.setTarget(mPanel);
mHideAnimator = AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_exit);
mHideAnimator.setTarget(mPanel);
mHideAnimator.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Animation is still in running state at this point.
hideAllInternal();
}
});
mShowDurationMillis =
mActivity.getResources().getInteger(R.integer.side_panel_show_duration);
mAutoHideScheduler = new AutoHideScheduler(activity, () -> hideAll(true));
}
public int getCount() {
return mFragmentCount;
}
public boolean isActive() {
return mFragmentCount != 0 && !isHiding();
}
public boolean isHiding() {
return mHideAnimator.isStarted();
}
/** Shows the given {@link SideFragment}. */
public void show(SideFragment sideFragment) {
show(sideFragment, true);
}
/** Shows the given {@link SideFragment}. */
public void show(SideFragment sideFragment, boolean showEnterAnimation) {
if (isHiding()) {
mHideAnimator.end();
}
boolean isFirst = (mFragmentCount == 0);
FragmentTransaction ft = mFragmentManager.beginTransaction();
if (!isFirst) {
ft.setCustomAnimations(
showEnterAnimation ? R.animator.side_panel_fragment_enter : 0,
R.animator.side_panel_fragment_exit,
R.animator.side_panel_fragment_pop_enter,
R.animator.side_panel_fragment_pop_exit);
}
ft.replace(R.id.side_fragment_container, sideFragment)
.addToBackStack(Integer.toString(mFragmentCount))
.commit();
mFragmentCount++;
if (isFirst) {
// We should wait for fragment transition and intital layouting finished to start the
// slide-in animation to prevent jankiness resulted by performing transition and
// layouting at the same time with animation.
mPanel.setVisibility(View.VISIBLE);
mShowOnGlobalLayoutListener =
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mPanel.getViewTreeObserver().removeOnGlobalLayoutListener(this);
mShowOnGlobalLayoutListener = null;
if (mPreShowRunnable != null) {
mPreShowRunnable.run();
}
mShowAnimator.start();
}
};
mPanel.getViewTreeObserver().addOnGlobalLayoutListener(mShowOnGlobalLayoutListener);
}
scheduleHideAll();
}
public void popSideFragment() {
if (!isActive()) {
return;
} else if (mFragmentCount == 1) {
// Show closing animation with the last fragment.
hideAll(true);
return;
}
mFragmentManager.popBackStack();
mFragmentCount--;
}
public void hideAll(boolean withAnimation) {
if (mShowAnimator.isStarted()) {
mShowAnimator.end();
}
if (mShowOnGlobalLayoutListener != null) {
// The show operation maybe requested but the show animator is not started yet, in this
// case, we show still run mPreShowRunnable.
mPanel.getViewTreeObserver().removeOnGlobalLayoutListener(mShowOnGlobalLayoutListener);
mShowOnGlobalLayoutListener = null;
if (mPreShowRunnable != null) {
mPreShowRunnable.run();
}
}
if (withAnimation) {
if (!isHiding()) {
mHideAnimator.start();
}
return;
}
if (isHiding()) {
mHideAnimator.end();
return;
}
hideAllInternal();
}
private void hideAllInternal() {
mAutoHideScheduler.cancel();
if (mFragmentCount == 0) {
return;
}
mPanel.setVisibility(View.GONE);
mFragmentManager.popBackStack(
FIRST_BACKSTACK_RECORD_NAME, FragmentManager.POP_BACK_STACK_INCLUSIVE);
mFragmentCount = 0;
if (mPostHideRunnable != null) {
mPostHideRunnable.run();
}
}
/**
* Show the side panel with animation. If there are many entries in the fragment stack, the
* animation look like that there's only one fragment.
*
* @param withAnimation specifies if animation should be shown.
*/
public void showSidePanel(boolean withAnimation) {
if (mFragmentCount == 0) {
return;
}
mPanel.setVisibility(View.VISIBLE);
if (withAnimation) {
mShowAnimator.start();
}
scheduleHideAll();
}
/**
* Hide the side panel. This method just hide the panel and preserves the back stack. If you
* want to empty the back stack, call {@link #hideAll}.
*/
public void hideSidePanel(boolean withAnimation) {
mAutoHideScheduler.cancel();
if (withAnimation) {
Animator hideAnimator =
AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_exit);
hideAnimator.setTarget(mPanel);
hideAnimator.start();
hideAnimator.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mPanel.setVisibility(View.GONE);
}
});
} else {
mPanel.setVisibility(View.GONE);
}
}
public boolean isSidePanelVisible() {
return mPanel.getVisibility() == View.VISIBLE;
}
/** Resets the timer for hiding side fragment. */
public void scheduleHideAll() {
mAutoHideScheduler.schedule(mShowDurationMillis);
}
/** Should {@code keyCode} hide the current panel. */
public boolean isHideKeyForCurrentPanel(int keyCode) {
if (isActive()) {
SideFragment current =
(SideFragment) mFragmentManager.findFragmentById(R.id.side_fragment_container);
return current != null && current.isHideKeyForThisPanel(keyCode);
}
return false;
}
@Override
public void onAccessibilityStateChanged(boolean enabled) {
mAutoHideScheduler.onAccessibilityStateChanged(enabled);
}
}