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