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.app.Fragment;
     20 import android.content.Context;
     21 import android.graphics.drawable.RippleDrawable;
     22 import android.os.Bundle;
     23 import android.support.v17.leanback.widget.VerticalGridView;
     24 import android.support.v7.widget.RecyclerView;
     25 import android.view.KeyEvent;
     26 import android.view.LayoutInflater;
     27 import android.view.View;
     28 import android.view.ViewGroup;
     29 import android.widget.TextView;
     30 
     31 import com.android.tv.MainActivity;
     32 import com.android.tv.R;
     33 import com.android.tv.TvApplication;
     34 import com.android.tv.analytics.DurationTimer;
     35 import com.android.tv.analytics.HasTrackerLabel;
     36 import com.android.tv.analytics.Tracker;
     37 import com.android.tv.data.ChannelDataManager;
     38 import com.android.tv.data.ProgramDataManager;
     39 import com.android.tv.util.SystemProperties;
     40 
     41 import java.util.List;
     42 
     43 public abstract class SideFragment extends Fragment implements HasTrackerLabel {
     44     public static final int INVALID_POSITION = -1;
     45 
     46     private static final int RECYCLED_VIEW_POOL_SIZE = 7;
     47     private static final int[] PRELOADED_VIEW_IDS = {
     48         R.layout.option_item_radio_button,
     49         R.layout.option_item_channel_lock,
     50         R.layout.option_item_check_box,
     51         R.layout.option_item_channel_check
     52     };
     53 
     54     private static RecyclerView.RecycledViewPool sRecycledViewPool;
     55 
     56     private VerticalGridView mListView;
     57     private ItemAdapter mAdapter;
     58     private SideFragmentListener mListener;
     59     private ChannelDataManager mChannelDataManager;
     60     private ProgramDataManager mProgramDataManager;
     61     private Tracker mTracker;
     62     private final DurationTimer mSidePanelDurationTimer = new DurationTimer();
     63 
     64     private final int mHideKey;
     65     private final int mDebugHideKey;
     66 
     67     public SideFragment() {
     68         this(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.KEYCODE_UNKNOWN);
     69     }
     70 
     71     /**
     72      * @param hideKey the KeyCode used to hide the fragment
     73      * @param debugHideKey the KeyCode used to hide the fragment if
     74      *            {@link SystemProperties#USE_DEBUG_KEYS}.
     75      */
     76     public SideFragment(int hideKey, int debugHideKey) {
     77         mHideKey = hideKey;
     78         mDebugHideKey = debugHideKey;
     79     }
     80 
     81     @Override
     82     public void onAttach(Context context) {
     83         super.onAttach(context);
     84         mChannelDataManager = getMainActivity().getChannelDataManager();
     85         mProgramDataManager = getMainActivity().getProgramDataManager();
     86         mTracker = TvApplication.getSingletons(context).getTracker();
     87     }
     88 
     89     @Override
     90     public View onCreateView(LayoutInflater inflater, ViewGroup container,
     91             Bundle savedInstanceState) {
     92         if (sRecycledViewPool == null) {
     93             // sRecycledViewPool should be initialized by calling preloadRecycledViews()
     94             // before the entering animation of this fragment starts,
     95             // because it takes long time and if it is called after the animation starts (e.g. here)
     96             // it can affect the animation.
     97             throw new IllegalStateException("The RecyclerView pool has not been initialized.");
     98         }
     99         View view = inflater.inflate(getFragmentLayoutResourceId(), container, false);
    100 
    101         TextView textView = (TextView) view.findViewById(R.id.side_panel_title);
    102         textView.setText(getTitle());
    103 
    104         mListView = (VerticalGridView) view.findViewById(R.id.side_panel_list);
    105         mListView.setRecycledViewPool(sRecycledViewPool);
    106 
    107         mAdapter = new ItemAdapter(inflater, getItemList());
    108         mListView.setAdapter(mAdapter);
    109         mListView.requestFocus();
    110 
    111         return view;
    112     }
    113 
    114     @Override
    115     public void onResume() {
    116         super.onResume();
    117         mTracker.sendShowSidePanel(this);
    118         mTracker.sendScreenView(this.getTrackerLabel());
    119         mSidePanelDurationTimer.start();
    120     }
    121 
    122     @Override
    123     public void onPause() {
    124         mTracker.sendHideSidePanel(this, mSidePanelDurationTimer.reset());
    125         super.onPause();
    126     }
    127 
    128     @Override
    129     public void onDetach() {
    130         mTracker = null;
    131         super.onDetach();
    132     }
    133 
    134     public final boolean isHideKeyForThisPanel(int keyCode) {
    135         boolean debugKeysEnabled = SystemProperties.USE_DEBUG_KEYS.getValue();
    136         return mHideKey != KeyEvent.KEYCODE_UNKNOWN &&
    137                 (mHideKey == keyCode || (debugKeysEnabled && mDebugHideKey == keyCode));
    138     }
    139 
    140     @Override
    141     public void onDestroyView() {
    142         super.onDestroyView();
    143         mListView.swapAdapter(null, true);
    144         if (mListener != null) {
    145             mListener.onSideFragmentViewDestroyed();
    146         }
    147     }
    148 
    149     public final void setListener(SideFragmentListener listener) {
    150         mListener = listener;
    151     }
    152 
    153     protected void setSelectedPosition(int position) {
    154         mListView.setSelectedPosition(position);
    155     }
    156 
    157     protected int getSelectedPosition() {
    158         return mListView.getSelectedPosition();
    159     }
    160 
    161     public void setItems(List<Item> items) {
    162         mAdapter.reset(items);
    163     }
    164 
    165     protected void closeFragment() {
    166         getMainActivity().getOverlayManager().getSideFragmentManager().popSideFragment();
    167     }
    168 
    169     protected MainActivity getMainActivity() {
    170         return (MainActivity) getActivity();
    171     }
    172 
    173     protected ChannelDataManager getChannelDataManager() {
    174         return mChannelDataManager;
    175     }
    176 
    177     protected ProgramDataManager getProgramDataManager() {
    178         return mProgramDataManager;
    179     }
    180 
    181     protected void notifyDataSetChanged() {
    182         mAdapter.notifyDataSetChanged();
    183     }
    184 
    185     /*
    186      * HACK: The following methods bypass the updating mechanism of RecyclerView.Adapter and
    187      * directly updates each item. This works around a bug in the base libraries where calling
    188      * Adapter.notifyItemsChanged() causes the VerticalGridView to lose track of displayed item
    189      * position.
    190      */
    191 
    192     protected void notifyItemChanged(int position) {
    193         notifyItemChanged(mAdapter.getItem(position));
    194     }
    195 
    196     protected void notifyItemChanged(Item item) {
    197         item.notifyUpdated();
    198     }
    199 
    200     /**
    201      * Notifies all items of ItemAdapter has changed without structural changes.
    202      */
    203     protected void notifyItemsChanged() {
    204         notifyItemsChanged(0, mAdapter.getItemCount());
    205     }
    206 
    207     /**
    208      * Notifies some items of ItemAdapter has changed starting from position
    209      * <code>positionStart</code> to the end without structural changes.
    210      */
    211     protected void notifyItemsChanged(int positionStart) {
    212         notifyItemsChanged(positionStart, mAdapter.getItemCount() - positionStart);
    213     }
    214 
    215     protected void notifyItemsChanged(int positionStart, int itemCount) {
    216         while (itemCount-- != 0) {
    217             notifyItemChanged(positionStart++);
    218         }
    219     }
    220 
    221     /*
    222      * END HACK
    223      */
    224 
    225     protected int getFragmentLayoutResourceId() {
    226         return R.layout.option_fragment;
    227     }
    228 
    229     protected abstract String getTitle();
    230     @Override
    231     public abstract String getTrackerLabel();
    232     protected abstract List<Item> getItemList();
    233 
    234     public interface SideFragmentListener {
    235         void onSideFragmentViewDestroyed();
    236     }
    237 
    238     /**
    239      * Preloads the view holders.
    240      */
    241     public static void preloadRecycledViews(Context context) {
    242         if (sRecycledViewPool != null) {
    243             return;
    244         }
    245         sRecycledViewPool = new RecyclerView.RecycledViewPool();
    246         LayoutInflater inflater =
    247                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    248         for (int id : PRELOADED_VIEW_IDS) {
    249             sRecycledViewPool.setMaxRecycledViews(id, RECYCLED_VIEW_POOL_SIZE);
    250             for (int j = 0; j < RECYCLED_VIEW_POOL_SIZE; ++j) {
    251                 ItemAdapter.ViewHolder viewHolder = new ItemAdapter.ViewHolder(
    252                         inflater.inflate(id, null, false));
    253                 sRecycledViewPool.putRecycledView(viewHolder);
    254             }
    255         }
    256     }
    257 
    258     /**
    259      * Releases the pre-loaded view holders.
    260      */
    261     public static void releasePreloadedRecycledViews() {
    262         sRecycledViewPool = null;
    263     }
    264 
    265     private static class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ViewHolder> {
    266         private final LayoutInflater mLayoutInflater;
    267         private List<Item> mItems;
    268 
    269         private ItemAdapter(LayoutInflater layoutInflater, List<Item> items) {
    270             mLayoutInflater = layoutInflater;
    271             mItems = items;
    272         }
    273 
    274         private void reset(List<Item> items) {
    275             mItems = items;
    276             notifyDataSetChanged();
    277         }
    278 
    279         @Override
    280         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    281             return new ViewHolder(mLayoutInflater.inflate(viewType, parent, false));
    282         }
    283 
    284         @Override
    285         public void onBindViewHolder(ViewHolder holder, int position) {
    286             holder.onBind(this, getItem(position));
    287         }
    288 
    289         @Override
    290         public void onViewRecycled(ViewHolder holder) {
    291             holder.onUnbind();
    292         }
    293 
    294         @Override
    295         public int getItemViewType(int position) {
    296             return getItem(position).getResourceId();
    297         }
    298 
    299         @Override
    300         public int getItemCount() {
    301             return mItems == null ? 0 : mItems.size();
    302         }
    303 
    304         private Item getItem(int position) {
    305             return mItems.get(position);
    306         }
    307 
    308         private void clearRadioGroup(Item item) {
    309             int position = mItems.indexOf(item);
    310             for (int i = position - 1; i >= 0; --i) {
    311                 if ((item = mItems.get(i)) instanceof RadioButtonItem) {
    312                     ((RadioButtonItem) item).setChecked(false);
    313                 } else {
    314                     break;
    315                 }
    316             }
    317             for (int i = position + 1; i < mItems.size(); ++i) {
    318                 if ((item = mItems.get(i)) instanceof RadioButtonItem) {
    319                     ((RadioButtonItem) item).setChecked(false);
    320                 } else {
    321                     break;
    322                 }
    323             }
    324         }
    325 
    326         private static class ViewHolder extends RecyclerView.ViewHolder
    327                 implements View.OnClickListener, View.OnFocusChangeListener {
    328             private ItemAdapter mAdapter;
    329             public Item mItem;
    330 
    331             private ViewHolder(View view) {
    332                 super(view);
    333                 itemView.setOnClickListener(this);
    334                 itemView.setOnFocusChangeListener(this);
    335             }
    336 
    337             public void onBind(ItemAdapter adapter, Item item) {
    338                 mAdapter = adapter;
    339                 mItem = item;
    340                 mItem.onBind(itemView);
    341                 mItem.onUpdate();
    342             }
    343 
    344             public void onUnbind() {
    345                 mItem.onUnbind();
    346                 mItem = null;
    347                 mAdapter = null;
    348             }
    349 
    350             @Override
    351             public void onClick(View view) {
    352                 if (mItem instanceof RadioButtonItem) {
    353                     mAdapter.clearRadioGroup(mItem);
    354                 }
    355                 if (view.getBackground() instanceof RippleDrawable) {
    356                     view.postDelayed(new Runnable() {
    357                         @Override
    358                         public void run() {
    359                             if (mItem != null) {
    360                                 mItem.onSelected();
    361                             }
    362                         }
    363                     }, view.getResources().getInteger(R.integer.side_panel_ripple_anim_duration));
    364                 } else {
    365                     mItem.onSelected();
    366                 }
    367             }
    368 
    369             @Override
    370             public void onFocusChange(View view, boolean focusGained) {
    371                 if (focusGained) {
    372                     mItem.onFocused();
    373                 }
    374             }
    375         }
    376     }
    377 }
    378