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 SideFragment()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 */ SideFragment(int hideKey, int debugHideKey)77 public SideFragment(int hideKey, int debugHideKey) { 78 mHideKey = hideKey; 79 mDebugHideKey = debugHideKey; 80 } 81 82 @Override onAttach(Activity activity)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 onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 onResume()116 public void onResume() { 117 super.onResume(); 118 mTracker.sendShowSidePanel(this); 119 mTracker.sendScreenView(this.getTrackerLabel()); 120 mSidePanelDurationTimer.start(); 121 } 122 123 @Override onPause()124 public void onPause() { 125 mTracker.sendHideSidePanel(this, mSidePanelDurationTimer.reset()); 126 super.onPause(); 127 } 128 129 @Override onDetach()130 public void onDetach() { 131 mTracker = null; 132 super.onDetach(); 133 } 134 isHideKeyForThisPanel(int keyCode)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 onDestroyView()142 public void onDestroyView() { 143 super.onDestroyView(); 144 mListView.swapAdapter(null, true); 145 if (mListener != null) { 146 mListener.onSideFragmentViewDestroyed(); 147 } 148 } 149 setListener(SideFragmentListener listener)150 public final void setListener(SideFragmentListener listener) { 151 mListener = listener; 152 } 153 setSelectedPosition(int position)154 protected void setSelectedPosition(int position) { 155 mListView.setSelectedPosition(position); 156 } 157 getSelectedPosition()158 protected int getSelectedPosition() { 159 return mListView.getSelectedPosition(); 160 } 161 setItems(List<Item> items)162 public void setItems(List<Item> items) { 163 mAdapter.reset(items); 164 } 165 closeFragment()166 protected void closeFragment() { 167 getMainActivity().getOverlayManager().getSideFragmentManager().popSideFragment(); 168 } 169 getMainActivity()170 protected MainActivity getMainActivity() { 171 return (MainActivity) getActivity(); 172 } 173 getChannelDataManager()174 protected ChannelDataManager getChannelDataManager() { 175 return mChannelDataManager; 176 } 177 getProgramDataManager()178 protected ProgramDataManager getProgramDataManager() { 179 return mProgramDataManager; 180 } 181 notifyDataSetChanged()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 notifyItemChanged(int position)193 protected void notifyItemChanged(int position) { 194 notifyItemChanged(mAdapter.getItem(position)); 195 } 196 notifyItemChanged(Item item)197 protected void notifyItemChanged(Item item) { 198 item.notifyUpdated(); 199 } 200 201 /** 202 * Notifies all items of ItemAdapter has changed without structural changes. 203 */ notifyItemsChanged()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 */ notifyItemsChanged(int positionStart)212 protected void notifyItemsChanged(int positionStart) { 213 notifyItemsChanged(positionStart, mAdapter.getItemCount() - positionStart); 214 } 215 notifyItemsChanged(int positionStart, int itemCount)216 protected void notifyItemsChanged(int positionStart, int itemCount) { 217 while (itemCount-- != 0) { 218 notifyItemChanged(positionStart++); 219 } 220 } 221 222 /* 223 * END HACK 224 */ 225 getFragmentLayoutResourceId()226 protected int getFragmentLayoutResourceId() { 227 return R.layout.option_fragment; 228 } 229 getTitle()230 protected abstract String getTitle(); 231 @Override getTrackerLabel()232 public abstract String getTrackerLabel(); getItemList()233 protected abstract List<Item> getItemList(); 234 235 public interface SideFragmentListener { onSideFragmentViewDestroyed()236 void onSideFragmentViewDestroyed(); 237 } 238 preloadRecycledViews(Context context)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 ItemAdapter(LayoutInflater layoutInflater, List<Item> items)260 private ItemAdapter(LayoutInflater layoutInflater, List<Item> items) { 261 mLayoutInflater = layoutInflater; 262 mItems = items; 263 } 264 reset(List<Item> items)265 private void reset(List<Item> items) { 266 mItems = items; 267 notifyDataSetChanged(); 268 } 269 270 @Override onCreateViewHolder(ViewGroup parent, int viewType)271 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 272 return new ViewHolder(mLayoutInflater.inflate(viewType, parent, false)); 273 } 274 275 @Override onBindViewHolder(ViewHolder holder, int position)276 public void onBindViewHolder(ViewHolder holder, int position) { 277 holder.onBind(this, getItem(position)); 278 } 279 280 @Override onViewRecycled(ViewHolder holder)281 public void onViewRecycled(ViewHolder holder) { 282 holder.onUnbind(); 283 } 284 285 @Override getItemViewType(int position)286 public int getItemViewType(int position) { 287 return getItem(position).getResourceId(); 288 } 289 290 @Override getItemCount()291 public int getItemCount() { 292 return mItems == null ? 0 : mItems.size(); 293 } 294 getItem(int position)295 private Item getItem(int position) { 296 return mItems.get(position); 297 } 298 clearRadioGroup(Item item)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 ViewHolder(View view)322 private ViewHolder(View view) { 323 super(view); 324 itemView.setOnClickListener(this); 325 itemView.setOnFocusChangeListener(this); 326 } 327 onBind(ItemAdapter adapter, Item item)328 public void onBind(ItemAdapter adapter, Item item) { 329 mAdapter = adapter; 330 mItem = item; 331 mItem.onBind(itemView); 332 mItem.onUpdate(); 333 } 334 onUnbind()335 public void onUnbind() { 336 mItem.onUnbind(); 337 mItem = null; 338 mAdapter = null; 339 } 340 341 @Override onClick(View view)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 onFocusChange(View view, boolean focusGained)361 public void onFocusChange(View view, boolean focusGained) { 362 if (focusGained) { 363 mItem.onFocused(); 364 } 365 } 366 } 367 } 368 } 369