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