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