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