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