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 SideFragment()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 */ SideFragment(int hideKey, int debugHideKey)78 public SideFragment(int hideKey, int debugHideKey) { 79 mHideKey = hideKey; 80 mDebugHideKey = debugHideKey; 81 } 82 83 @Override onAttach(Context context)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 onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 onResume()112 public void onResume() { 113 super.onResume(); 114 mTracker.sendShowSidePanel(this); 115 mTracker.sendScreenView(this.getTrackerLabel()); 116 mSidePanelDurationTimer.start(); 117 } 118 119 @Override onPause()120 public void onPause() { 121 mTracker.sendHideSidePanel(this, mSidePanelDurationTimer.reset()); 122 super.onPause(); 123 } 124 125 @Override onDetach()126 public void onDetach() { 127 mTracker = null; 128 super.onDetach(); 129 } 130 isHideKeyForThisPanel(int keyCode)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 onDestroyView()138 public void onDestroyView() { 139 super.onDestroyView(); 140 mListView.swapAdapter(null, true); 141 if (mListener != null) { 142 mListener.onSideFragmentViewDestroyed(); 143 } 144 } 145 setListener(SideFragmentListener listener)146 public final void setListener(SideFragmentListener listener) { 147 mListener = listener; 148 } 149 setSelectedPosition(int position)150 protected void setSelectedPosition(int position) { 151 mListView.setSelectedPosition(position); 152 } 153 getSelectedPosition()154 protected int getSelectedPosition() { 155 return mListView.getSelectedPosition(); 156 } 157 setItems(List<T> items)158 public void setItems(List<T> items) { 159 mAdapter.reset(items); 160 } 161 closeFragment()162 protected void closeFragment() { 163 getMainActivity().getOverlayManager().getSideFragmentManager().popSideFragment(); 164 } 165 getMainActivity()166 protected MainActivity getMainActivity() { 167 return (MainActivity) getActivity(); 168 } 169 getChannelDataManager()170 protected ChannelDataManager getChannelDataManager() { 171 return mChannelDataManager; 172 } 173 getProgramDataManager()174 protected ProgramDataManager getProgramDataManager() { 175 return mProgramDataManager; 176 } 177 notifyDataSetChanged()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 notifyItemChanged(int position)189 protected void notifyItemChanged(int position) { 190 notifyItemChanged(mAdapter.getItem(position)); 191 } 192 notifyItemChanged(Item item)193 protected void notifyItemChanged(Item item) { 194 item.notifyUpdated(); 195 } 196 197 /** Notifies all items of ItemAdapter has changed without structural changes. */ notifyItemsChanged()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 */ notifyItemsChanged(int positionStart)206 protected void notifyItemsChanged(int positionStart) { 207 notifyItemsChanged(positionStart, mAdapter.getItemCount() - positionStart); 208 } 209 notifyItemsChanged(int positionStart, int itemCount)210 protected void notifyItemsChanged(int positionStart, int itemCount) { 211 while (itemCount-- != 0) { 212 notifyItemChanged(positionStart++); 213 } 214 } 215 216 /* 217 * END HACK 218 */ 219 getFragmentLayoutResourceId()220 protected int getFragmentLayoutResourceId() { 221 return R.layout.option_fragment; 222 } 223 getTitle()224 protected abstract String getTitle(); 225 226 @Override getTrackerLabel()227 public abstract String getTrackerLabel(); 228 getItemList()229 protected abstract List<T> getItemList(); 230 231 public interface SideFragmentListener { onSideFragmentViewDestroyed()232 void onSideFragmentViewDestroyed(); 233 } 234 235 /** Preloads the item views. */ preloadItemViews(Context context)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. */ releaseRecycledViewPool()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 ItemAdapter(LayoutInflater layoutInflater, List<T> items)255 private ItemAdapter(LayoutInflater layoutInflater, List<T> items) { 256 mLayoutInflater = layoutInflater; 257 mItems = items; 258 } 259 reset(List<T> items)260 private void reset(List<T> items) { 261 mItems = items; 262 notifyDataSetChanged(); 263 } 264 265 @Override onCreateViewHolder(ViewGroup parent, int viewType)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 onBindViewHolder(ViewHolder holder, int position)272 public void onBindViewHolder(ViewHolder holder, int position) { 273 holder.onBind(this, getItem(position)); 274 } 275 276 @Override onViewRecycled(ViewHolder holder)277 public void onViewRecycled(ViewHolder holder) { 278 holder.onUnbind(); 279 } 280 281 @Override getItemViewType(int position)282 public int getItemViewType(int position) { 283 return getItem(position).getResourceId(); 284 } 285 286 @Override getItemCount()287 public int getItemCount() { 288 return mItems == null ? 0 : mItems.size(); 289 } 290 getItem(int position)291 private T getItem(int position) { 292 return mItems.get(position); 293 } 294 clearRadioGroup(T item)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 ViewHolder(View view)319 private ViewHolder(View view) { 320 super(view); 321 itemView.setOnClickListener(this); 322 itemView.setOnFocusChangeListener(this); 323 } 324 onBind(ItemAdapter adapter, Item item)325 public void onBind(ItemAdapter adapter, Item item) { 326 mAdapter = adapter; 327 mItem = item; 328 mItem.onBind(itemView); 329 mItem.onUpdate(); 330 } 331 onUnbind()332 public void onUnbind() { 333 mItem.onUnbind(); 334 mItem = null; 335 mAdapter = null; 336 } 337 338 @Override onClick(View view)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 () -> { 346 if (mItem != null) { 347 mItem.onSelected(); 348 } 349 }, 350 view.getResources().getInteger(R.integer.side_panel_ripple_anim_duration)); 351 } else { 352 mItem.onSelected(); 353 } 354 } 355 356 @Override onFocusChange(View view, boolean focusGained)357 public void onFocusChange(View view, boolean focusGained) { 358 if (focusGained) { 359 mItem.onFocused(); 360 } 361 } 362 } 363 } 364