Home | History | Annotate | Download | only in sidepanel
      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 
     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 
     78     public int getCount() {
     79         return mFragmentCount;
     80     }
     81 
     82     public boolean isActive() {
     83         return mFragmentCount != 0 && !isHiding();
     84     }
     85 
     86     public boolean isHiding() {
     87         return mHideAnimator.isStarted();
     88     }
     89 
     90     /** Shows the given {@link SideFragment}. */
     91     public void show(SideFragment sideFragment) {
     92         show(sideFragment, true);
     93     }
     94 
     95     /** Shows the given {@link SideFragment}. */
     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 
    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 
    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 
    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      */
    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      */
    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 
    231     public boolean isSidePanelVisible() {
    232         return mPanel.getVisibility() == View.VISIBLE;
    233     }
    234 
    235     /** Resets the timer for hiding side fragment. */
    236     public void scheduleHideAll() {
    237         mAutoHideScheduler.schedule(mShowDurationMillis);
    238     }
    239 
    240     /** Should {@code keyCode} hide the current panel. */
    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
    251     public void onAccessibilityStateChanged(boolean enabled) {
    252         mAutoHideScheduler.onAccessibilityStateChanged(enabled);
    253     }
    254 }
    255