1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14package android.support.v17.leanback.app; 15 16import android.content.Context; 17import android.database.DataSetObserver; 18import android.media.AudioManager; 19import android.support.v17.leanback.R; 20import android.support.v17.leanback.widget.GuidedAction; 21import android.support.v17.leanback.widget.GuidedActionsStylist; 22import android.support.v7.widget.RecyclerView; 23import android.support.v7.widget.RecyclerView.ViewHolder; 24import android.util.Log; 25import android.view.KeyEvent; 26import android.view.LayoutInflater; 27import android.view.View; 28import android.view.ViewGroup; 29import android.widget.AdapterView.OnItemSelectedListener; 30import android.widget.ImageView; 31import android.widget.TextView; 32 33import java.util.ArrayList; 34import java.util.List; 35 36/** 37 * GuidedActionAdapter instantiates views for guided actions, and manages their interactions. 38 * Presentation (view creation and state animation) is delegated to a {@link 39 * GuidedActionsStylist}, while clients are notified of interactions via 40 * {@link GuidedActionAdapter.ClickListener} and {@link GuidedActionAdapter.FocusListener}. 41 */ 42class GuidedActionAdapter extends RecyclerView.Adapter { 43 private static final String TAG = "GuidedActionAdapter"; 44 private static final boolean DEBUG = false; 45 46 /** 47 * Object listening for click events within a {@link GuidedActionAdapter}. 48 */ 49 public interface ClickListener { 50 51 /** 52 * Called when the user clicks on an action. 53 */ 54 public void onGuidedActionClicked(GuidedAction action); 55 } 56 57 /** 58 * Object listening for focus events within a {@link GuidedActionAdapter}. 59 */ 60 public interface FocusListener { 61 62 /** 63 * Called when the user focuses on an action. 64 */ 65 public void onGuidedActionFocused(GuidedAction action); 66 } 67 68 /** 69 * View holder containing a {@link GuidedAction}. 70 */ 71 private static class ActionViewHolder extends ViewHolder { 72 73 private final GuidedActionsStylist.ViewHolder mStylistViewHolder; 74 private GuidedAction mAction; 75 76 /** 77 * Constructs a view holder that can be associated with a GuidedAction. 78 */ 79 public ActionViewHolder(View v, GuidedActionsStylist.ViewHolder subViewHolder) { 80 super(v); 81 mStylistViewHolder = subViewHolder; 82 } 83 84 /** 85 * Retrieves the action associated with this view holder. 86 * @return The GuidedAction associated with this view holder. 87 */ 88 public GuidedAction getAction() { 89 return mAction; 90 } 91 92 /** 93 * Sets the action associated with this view holder. 94 * @param action The GuidedAction associated with this view holder. 95 */ 96 public void setAction(GuidedAction action) { 97 mAction = action; 98 } 99 } 100 101 private RecyclerView mRecyclerView; 102 private final ActionOnKeyListener mActionOnKeyListener; 103 private final ActionOnFocusListener mActionOnFocusListener; 104 private final List<GuidedAction> mActions; 105 private ClickListener mClickListener; 106 private GuidedActionsStylist mStylist; 107 private final View.OnClickListener mOnClickListener = new View.OnClickListener() { 108 @Override 109 public void onClick(View v) { 110 if (v != null && v.getWindowToken() != null && mClickListener != null) { 111 ActionViewHolder avh = (ActionViewHolder)mRecyclerView.getChildViewHolder(v); 112 GuidedAction action = avh.getAction(); 113 if (action.isEnabled() && !action.infoOnly()) { 114 mClickListener.onGuidedActionClicked(action); 115 } 116 } 117 } 118 }; 119 120 /** 121 * Constructs a GuidedActionAdapter with the given list of guided actions, the given click and 122 * focus listeners, and the given presenter. 123 * @param actions The list of guided actions this adapter will manage. 124 * @param clickListener The click listener for items in this adapter. 125 * @param focusListener The focus listener for items in this adapter. 126 * @param presenter The presenter that will manage the display of items in this adapter. 127 */ 128 public GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener, 129 FocusListener focusListener, GuidedActionsStylist presenter) { 130 super(); 131 mActions = new ArrayList<GuidedAction>(actions); 132 mClickListener = clickListener; 133 mStylist = presenter; 134 mActionOnKeyListener = new ActionOnKeyListener(clickListener, mActions); 135 mActionOnFocusListener = new ActionOnFocusListener(focusListener); 136 } 137 138 /** 139 * Sets the list of actions managed by this adapter. 140 * @param actions The list of actions to be managed. 141 */ 142 public void setActions(List<GuidedAction> actions) { 143 mActionOnFocusListener.unFocus(); 144 mActions.clear(); 145 mActions.addAll(actions); 146 notifyDataSetChanged(); 147 } 148 149 /** 150 * Returns the count of actions managed by this adapter. 151 * @return The count of actions managed by this adapter. 152 */ 153 public int getCount() { 154 return mActions.size(); 155 } 156 157 /** 158 * Returns the GuidedAction at the given position in the managed list. 159 * @param position The position of the desired GuidedAction. 160 * @return The GuidedAction at the given position. 161 */ 162 public GuidedAction getItem(int position) { 163 return mActions.get(position); 164 } 165 166 /** 167 * Sets the click listener for items managed by this adapter. 168 * @param clickListener The click listener for this adapter. 169 */ 170 public void setClickListener(ClickListener clickListener) { 171 mClickListener = clickListener; 172 mActionOnKeyListener.setListener(clickListener); 173 } 174 175 /** 176 * Sets the focus listener for items managed by this adapter. 177 * @param focusListener The focus listener for this adapter. 178 */ 179 public void setFocusListener(FocusListener focusListener) { 180 mActionOnFocusListener.setFocusListener(focusListener); 181 } 182 183 /** 184 * Used for serialization only. 185 * @hide 186 */ 187 public List<GuidedAction> getActions() { 188 return new ArrayList<GuidedAction>(mActions); 189 } 190 191 /** 192 * {@inheritDoc} 193 */ 194 @Override 195 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 196 mRecyclerView = recyclerView; 197 } 198 199 /** 200 * {@inheritDoc} 201 */ 202 @Override 203 public void onDetachedFromRecyclerView(RecyclerView recyclerView) { 204 mRecyclerView = null; 205 } 206 207 /** 208 * {@inheritDoc} 209 */ 210 @Override 211 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 212 GuidedActionsStylist.ViewHolder vh = mStylist.onCreateViewHolder(parent); 213 View v = vh.view; 214 v.setOnKeyListener(mActionOnKeyListener); 215 v.setOnClickListener(mOnClickListener); 216 v.setOnFocusChangeListener(mActionOnFocusListener); 217 218 return new ActionViewHolder(v, vh); 219 } 220 221 /** 222 * {@inheritDoc} 223 */ 224 @Override 225 public void onBindViewHolder(ViewHolder holder, int position) { 226 if (position >= mActions.size()) { 227 return; 228 } 229 ActionViewHolder avh = (ActionViewHolder)holder; 230 GuidedAction action = mActions.get(position); 231 avh.setAction(action); 232 mStylist.onBindViewHolder(avh.mStylistViewHolder, action); 233 } 234 235 /** 236 * {@inheritDoc} 237 */ 238 @Override 239 public int getItemCount() { 240 return mActions.size(); 241 } 242 243 private class ActionOnFocusListener implements View.OnFocusChangeListener { 244 245 private FocusListener mFocusListener; 246 private View mSelectedView; 247 248 ActionOnFocusListener(FocusListener focusListener) { 249 mFocusListener = focusListener; 250 } 251 252 public void setFocusListener(FocusListener focusListener) { 253 mFocusListener = focusListener; 254 } 255 256 public void unFocus() { 257 if (mSelectedView != null) { 258 ViewHolder vh = mRecyclerView.getChildViewHolder(mSelectedView); 259 if (vh != null) { 260 ActionViewHolder avh = (ActionViewHolder)vh; 261 mStylist.onAnimateItemFocused(avh.mStylistViewHolder, false); 262 } else { 263 Log.w(TAG, "RecyclerView returned null view holder", 264 new Throwable()); 265 } 266 } 267 } 268 269 @Override 270 public void onFocusChange(View v, boolean hasFocus) { 271 ActionViewHolder avh = (ActionViewHolder)mRecyclerView.getChildViewHolder(v); 272 mStylist.onAnimateItemFocused(avh.mStylistViewHolder, hasFocus); 273 if (hasFocus) { 274 mSelectedView = v; 275 if (mFocusListener != null) { 276 // We still call onGuidedActionFocused so that listeners can clear 277 // state if they want. 278 mFocusListener.onGuidedActionFocused(avh.getAction()); 279 } 280 } else { 281 if (mSelectedView == v) { 282 mSelectedView = null; 283 } 284 } 285 } 286 } 287 288 private class ActionOnKeyListener implements View.OnKeyListener { 289 290 private final List<GuidedAction> mActions; 291 private boolean mKeyPressed = false; 292 private ClickListener mClickListener; 293 294 public ActionOnKeyListener(ClickListener listener, 295 List<GuidedAction> actions) { 296 mClickListener = listener; 297 mActions = actions; 298 } 299 300 public void setListener(ClickListener listener) { 301 mClickListener = listener; 302 } 303 304 private void playSound(View v, int soundEffect) { 305 if (v.isSoundEffectsEnabled()) { 306 Context ctx = v.getContext(); 307 AudioManager manager = (AudioManager)ctx.getSystemService(Context.AUDIO_SERVICE); 308 manager.playSoundEffect(soundEffect); 309 } 310 } 311 312 /** 313 * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event. 314 */ 315 @Override 316 public boolean onKey(View v, int keyCode, KeyEvent event) { 317 if (v == null || event == null) { 318 return false; 319 } 320 boolean handled = false; 321 switch (keyCode) { 322 case KeyEvent.KEYCODE_DPAD_CENTER: 323 case KeyEvent.KEYCODE_NUMPAD_ENTER: 324 case KeyEvent.KEYCODE_BUTTON_X: 325 case KeyEvent.KEYCODE_BUTTON_Y: 326 case KeyEvent.KEYCODE_ENTER: 327 328 ActionViewHolder avh = (ActionViewHolder)mRecyclerView.getChildViewHolder(v); 329 GuidedAction action = avh.getAction(); 330 331 if (!action.isEnabled() || action.infoOnly()) { 332 if (event.getAction() == KeyEvent.ACTION_DOWN) { 333 // TODO: requires API 19 334 //playSound(v, AudioManager.FX_KEYPRESS_INVALID); 335 } 336 return true; 337 } 338 339 switch (event.getAction()) { 340 case KeyEvent.ACTION_DOWN: 341 if (!mKeyPressed) { 342 mKeyPressed = true; 343 344 playSound(v, AudioManager.FX_KEY_CLICK); 345 346 if (DEBUG) { 347 Log.d(TAG, "Enter Key down"); 348 } 349 350 mStylist.onAnimateItemPressed(avh.mStylistViewHolder, 351 mKeyPressed); 352 handled = true; 353 } 354 break; 355 case KeyEvent.ACTION_UP: 356 if (mKeyPressed) { 357 mKeyPressed = false; 358 359 if (DEBUG) { 360 Log.d(TAG, "Enter Key up"); 361 } 362 363 mStylist.onAnimateItemPressed(avh.mStylistViewHolder, 364 mKeyPressed); 365 handleCheckedActions(avh, action); 366 mClickListener.onGuidedActionClicked(action); 367 handled = true; 368 } 369 break; 370 default: 371 break; 372 } 373 break; 374 default: 375 break; 376 } 377 return handled; 378 } 379 380 private void handleCheckedActions(ActionViewHolder avh, GuidedAction action) { 381 int actionCheckSetId = action.getCheckSetId(); 382 if (actionCheckSetId != GuidedAction.NO_CHECK_SET) { 383 // Find any actions that are checked and are in the same group 384 // as the selected action. Fade their checkmarks out. 385 for (int i = 0, size = mActions.size(); i < size; i++) { 386 GuidedAction a = mActions.get(i); 387 if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) { 388 a.setChecked(false); 389 ViewHolder vh = mRecyclerView.findViewHolderForPosition(i); 390 if (vh != null) { 391 GuidedActionsStylist.ViewHolder subViewHolder = 392 ((ActionViewHolder)vh).mStylistViewHolder; 393 mStylist.onAnimateItemChecked(subViewHolder, false); 394 } 395 } 396 } 397 398 // If we we'ren't already checked, fade our checkmark in. 399 if (!action.isChecked()) { 400 action.setChecked(true); 401 mStylist.onAnimateItemChecked(avh.mStylistViewHolder, true); 402 } 403 } 404 } 405 } 406} 407