GuidedActionAdapter.java revision a51a405279fb81135abbb7c25ba431842582c8c8
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.widget; 15 16import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 17 18import android.support.annotation.RestrictTo; 19import android.support.v7.widget.RecyclerView; 20import android.support.v7.widget.RecyclerView.ViewHolder; 21import android.util.Log; 22import android.view.KeyEvent; 23import android.view.View; 24import android.view.ViewGroup; 25import android.view.ViewParent; 26import android.view.inputmethod.EditorInfo; 27import android.widget.EditText; 28import android.widget.TextView; 29import android.widget.TextView.OnEditorActionListener; 30 31import java.util.ArrayList; 32import java.util.List; 33 34/** 35 * GuidedActionAdapter instantiates views for guided actions, and manages their interactions. 36 * Presentation (view creation and state animation) is delegated to a {@link 37 * GuidedActionsStylist}, while clients are notified of interactions via 38 * {@link GuidedActionAdapter.ClickListener} and {@link GuidedActionAdapter.FocusListener}. 39 * @hide 40 */ 41@RestrictTo(LIBRARY_GROUP) 42public class GuidedActionAdapter extends RecyclerView.Adapter { 43 static final String TAG = "GuidedActionAdapter"; 44 static final boolean DEBUG = false; 45 46 static final String TAG_EDIT = "EditableAction"; 47 static final boolean DEBUG_EDIT = false; 48 49 /** 50 * Object listening for click events within a {@link GuidedActionAdapter}. 51 */ 52 public interface ClickListener { 53 54 /** 55 * Called when the user clicks on an action. 56 */ 57 public void onGuidedActionClicked(GuidedAction action); 58 59 } 60 61 /** 62 * Object listening for focus events within a {@link GuidedActionAdapter}. 63 */ 64 public interface FocusListener { 65 66 /** 67 * Called when the user focuses on an action. 68 */ 69 public void onGuidedActionFocused(GuidedAction action); 70 } 71 72 /** 73 * Object listening for edit events within a {@link GuidedActionAdapter}. 74 */ 75 public interface EditListener { 76 77 /** 78 * Called when the user exits edit mode on an action. 79 */ 80 public void onGuidedActionEditCanceled(GuidedAction action); 81 82 /** 83 * Called when the user exits edit mode on an action and process confirm button in IME. 84 */ 85 public long onGuidedActionEditedAndProceed(GuidedAction action); 86 87 /** 88 * Called when Ime Open 89 */ 90 public void onImeOpen(); 91 92 /** 93 * Called when Ime Close 94 */ 95 public void onImeClose(); 96 } 97 98 private final boolean mIsSubAdapter; 99 private final ActionOnKeyListener mActionOnKeyListener; 100 private final ActionOnFocusListener mActionOnFocusListener; 101 private final ActionEditListener mActionEditListener; 102 private final List<GuidedAction> mActions; 103 private ClickListener mClickListener; 104 final GuidedActionsStylist mStylist; 105 private final View.OnClickListener mOnClickListener = new View.OnClickListener() { 106 @Override 107 public void onClick(View v) { 108 if (v != null && v.getWindowToken() != null && getRecyclerView() != null) { 109 GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder) 110 getRecyclerView().getChildViewHolder(v); 111 GuidedAction action = avh.getAction(); 112 if (action.hasTextEditable()) { 113 if (DEBUG_EDIT) Log.v(TAG_EDIT, "openIme by click"); 114 mGroup.openIme(GuidedActionAdapter.this, avh); 115 } else if (action.hasEditableActivatorView()) { 116 if (DEBUG_EDIT) Log.v(TAG_EDIT, "toggle editing mode by click"); 117 performOnActionClick(avh); 118 } else { 119 handleCheckedActions(avh); 120 if (action.isEnabled() && !action.infoOnly()) { 121 performOnActionClick(avh); 122 } 123 } 124 } 125 } 126 }; 127 GuidedActionAdapterGroup mGroup; 128 129 /** 130 * Constructs a GuidedActionAdapter with the given list of guided actions, the given click and 131 * focus listeners, and the given presenter. 132 * @param actions The list of guided actions this adapter will manage. 133 * @param focusListener The focus listener for items in this adapter. 134 * @param presenter The presenter that will manage the display of items in this adapter. 135 */ 136 public GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener, 137 FocusListener focusListener, GuidedActionsStylist presenter, boolean isSubAdapter) { 138 super(); 139 mActions = actions == null ? new ArrayList<GuidedAction>() : 140 new ArrayList<GuidedAction>(actions); 141 mClickListener = clickListener; 142 mStylist = presenter; 143 mActionOnKeyListener = new ActionOnKeyListener(); 144 mActionOnFocusListener = new ActionOnFocusListener(focusListener); 145 mActionEditListener = new ActionEditListener(); 146 mIsSubAdapter = isSubAdapter; 147 } 148 149 /** 150 * Sets the list of actions managed by this adapter. 151 * @param actions The list of actions to be managed. 152 */ 153 public void setActions(List<GuidedAction> actions) { 154 mStylist.collapseAction(false); 155 mActionOnFocusListener.unFocus(); 156 mActions.clear(); 157 mActions.addAll(actions); 158 notifyDataSetChanged(); 159 } 160 161 /** 162 * Returns the count of actions managed by this adapter. 163 * @return The count of actions managed by this adapter. 164 */ 165 public int getCount() { 166 return mActions.size(); 167 } 168 169 /** 170 * Returns the GuidedAction at the given position in the managed list. 171 * @param position The position of the desired GuidedAction. 172 * @return The GuidedAction at the given position. 173 */ 174 public GuidedAction getItem(int position) { 175 return mActions.get(position); 176 } 177 178 /** 179 * Return index of action in array 180 * @param action Action to search index. 181 * @return Index of Action in array. 182 */ 183 public int indexOf(GuidedAction action) { 184 return mActions.indexOf(action); 185 } 186 187 /** 188 * @return GuidedActionsStylist used to build the actions list UI. 189 */ 190 public GuidedActionsStylist getGuidedActionsStylist() { 191 return mStylist; 192 } 193 194 /** 195 * Sets the click listener for items managed by this adapter. 196 * @param clickListener The click listener for this adapter. 197 */ 198 public void setClickListener(ClickListener clickListener) { 199 mClickListener = clickListener; 200 } 201 202 /** 203 * Sets the focus listener for items managed by this adapter. 204 * @param focusListener The focus listener for this adapter. 205 */ 206 public void setFocusListener(FocusListener focusListener) { 207 mActionOnFocusListener.setFocusListener(focusListener); 208 } 209 210 /** 211 * Used for serialization only. 212 * @hide 213 */ 214 @RestrictTo(LIBRARY_GROUP) 215 public List<GuidedAction> getActions() { 216 return new ArrayList<GuidedAction>(mActions); 217 } 218 219 /** 220 * {@inheritDoc} 221 */ 222 @Override 223 public int getItemViewType(int position) { 224 return mStylist.getItemViewType(mActions.get(position)); 225 } 226 227 RecyclerView getRecyclerView() { 228 return mIsSubAdapter ? mStylist.getSubActionsGridView() : mStylist.getActionsGridView(); 229 } 230 231 /** 232 * {@inheritDoc} 233 */ 234 @Override 235 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 236 GuidedActionsStylist.ViewHolder vh = mStylist.onCreateViewHolder(parent, viewType); 237 View v = vh.itemView; 238 v.setOnKeyListener(mActionOnKeyListener); 239 v.setOnClickListener(mOnClickListener); 240 v.setOnFocusChangeListener(mActionOnFocusListener); 241 242 setupListeners(vh.getEditableTitleView()); 243 setupListeners(vh.getEditableDescriptionView()); 244 245 return vh; 246 } 247 248 private void setupListeners(EditText edit) { 249 if (edit != null) { 250 edit.setPrivateImeOptions("EscapeNorth=1;"); 251 edit.setOnEditorActionListener(mActionEditListener); 252 if (edit instanceof ImeKeyMonitor) { 253 ImeKeyMonitor monitor = (ImeKeyMonitor)edit; 254 monitor.setImeKeyListener(mActionEditListener); 255 } 256 } 257 } 258 259 /** 260 * {@inheritDoc} 261 */ 262 @Override 263 public void onBindViewHolder(ViewHolder holder, int position) { 264 if (position >= mActions.size()) { 265 return; 266 } 267 final GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)holder; 268 GuidedAction action = mActions.get(position); 269 mStylist.onBindViewHolder(avh, action); 270 } 271 272 /** 273 * {@inheritDoc} 274 */ 275 @Override 276 public int getItemCount() { 277 return mActions.size(); 278 } 279 280 private class ActionOnFocusListener implements View.OnFocusChangeListener { 281 282 private FocusListener mFocusListener; 283 private View mSelectedView; 284 285 ActionOnFocusListener(FocusListener focusListener) { 286 mFocusListener = focusListener; 287 } 288 289 public void setFocusListener(FocusListener focusListener) { 290 mFocusListener = focusListener; 291 } 292 293 public void unFocus() { 294 if (mSelectedView != null && getRecyclerView() != null) { 295 ViewHolder vh = getRecyclerView().getChildViewHolder(mSelectedView); 296 if (vh != null) { 297 GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)vh; 298 mStylist.onAnimateItemFocused(avh, false); 299 } else { 300 Log.w(TAG, "RecyclerView returned null view holder", 301 new Throwable()); 302 } 303 } 304 } 305 306 @Override 307 public void onFocusChange(View v, boolean hasFocus) { 308 if (getRecyclerView() == null) { 309 return; 310 } 311 GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder) 312 getRecyclerView().getChildViewHolder(v); 313 if (hasFocus) { 314 mSelectedView = v; 315 if (mFocusListener != null) { 316 // We still call onGuidedActionFocused so that listeners can clear 317 // state if they want. 318 mFocusListener.onGuidedActionFocused(avh.getAction()); 319 } 320 } else { 321 if (mSelectedView == v) { 322 mStylist.onAnimateItemPressedCancelled(avh); 323 mSelectedView = null; 324 } 325 } 326 mStylist.onAnimateItemFocused(avh, hasFocus); 327 } 328 } 329 330 public GuidedActionsStylist.ViewHolder findSubChildViewHolder(View v) { 331 // Needed because RecyclerView.getChildViewHolder does not traverse the hierarchy 332 if (getRecyclerView() == null) { 333 return null; 334 } 335 GuidedActionsStylist.ViewHolder result = null; 336 ViewParent parent = v.getParent(); 337 while (parent != getRecyclerView() && parent != null && v != null) { 338 v = (View)parent; 339 parent = parent.getParent(); 340 } 341 if (parent != null && v != null) { 342 result = (GuidedActionsStylist.ViewHolder)getRecyclerView().getChildViewHolder(v); 343 } 344 return result; 345 } 346 347 public void handleCheckedActions(GuidedActionsStylist.ViewHolder avh) { 348 GuidedAction action = avh.getAction(); 349 int actionCheckSetId = action.getCheckSetId(); 350 if (getRecyclerView() != null && actionCheckSetId != GuidedAction.NO_CHECK_SET) { 351 // Find any actions that are checked and are in the same group 352 // as the selected action. Fade their checkmarks out. 353 if (actionCheckSetId != GuidedAction.CHECKBOX_CHECK_SET_ID) { 354 for (int i = 0, size = mActions.size(); i < size; i++) { 355 GuidedAction a = mActions.get(i); 356 if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) { 357 a.setChecked(false); 358 GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder) 359 getRecyclerView().findViewHolderForPosition(i); 360 if (vh != null) { 361 mStylist.onAnimateItemChecked(vh, false); 362 } 363 } 364 } 365 } 366 367 // If we we'ren't already checked, fade our checkmark in. 368 if (!action.isChecked()) { 369 action.setChecked(true); 370 mStylist.onAnimateItemChecked(avh, true); 371 } else { 372 if (actionCheckSetId == GuidedAction.CHECKBOX_CHECK_SET_ID) { 373 action.setChecked(false); 374 mStylist.onAnimateItemChecked(avh, false); 375 } 376 } 377 } 378 } 379 380 public void performOnActionClick(GuidedActionsStylist.ViewHolder avh) { 381 if (mClickListener != null) { 382 mClickListener.onGuidedActionClicked(avh.getAction()); 383 } 384 } 385 386 private class ActionOnKeyListener implements View.OnKeyListener { 387 388 private boolean mKeyPressed = false; 389 390 ActionOnKeyListener() { 391 } 392 393 /** 394 * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event. 395 */ 396 @Override 397 public boolean onKey(View v, int keyCode, KeyEvent event) { 398 if (v == null || event == null || getRecyclerView() == null) { 399 return false; 400 } 401 boolean handled = false; 402 switch (keyCode) { 403 case KeyEvent.KEYCODE_DPAD_CENTER: 404 case KeyEvent.KEYCODE_NUMPAD_ENTER: 405 case KeyEvent.KEYCODE_BUTTON_X: 406 case KeyEvent.KEYCODE_BUTTON_Y: 407 case KeyEvent.KEYCODE_ENTER: 408 409 GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder) 410 getRecyclerView().getChildViewHolder(v); 411 GuidedAction action = avh.getAction(); 412 413 if (!action.isEnabled() || action.infoOnly()) { 414 if (event.getAction() == KeyEvent.ACTION_DOWN) { 415 // TODO: requires API 19 416 //playSound(v, AudioManager.FX_KEYPRESS_INVALID); 417 } 418 return true; 419 } 420 421 switch (event.getAction()) { 422 case KeyEvent.ACTION_DOWN: 423 if (DEBUG) { 424 Log.d(TAG, "Enter Key down"); 425 } 426 if (!mKeyPressed) { 427 mKeyPressed = true; 428 mStylist.onAnimateItemPressed(avh, mKeyPressed); 429 } 430 break; 431 case KeyEvent.ACTION_UP: 432 if (DEBUG) { 433 Log.d(TAG, "Enter Key up"); 434 } 435 // Sometimes we are losing ACTION_DOWN for the first ENTER after pressed 436 // Escape in IME. 437 if (mKeyPressed) { 438 mKeyPressed = false; 439 mStylist.onAnimateItemPressed(avh, mKeyPressed); 440 } 441 break; 442 default: 443 break; 444 } 445 break; 446 default: 447 break; 448 } 449 return handled; 450 } 451 452 } 453 454 private class ActionEditListener implements OnEditorActionListener, 455 ImeKeyMonitor.ImeKeyListener { 456 457 ActionEditListener() { 458 } 459 460 @Override 461 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 462 if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME action: " + actionId); 463 boolean handled = false; 464 if (actionId == EditorInfo.IME_ACTION_NEXT 465 || actionId == EditorInfo.IME_ACTION_DONE) { 466 mGroup.fillAndGoNext(GuidedActionAdapter.this, v); 467 handled = true; 468 } else if (actionId == EditorInfo.IME_ACTION_NONE) { 469 if (DEBUG_EDIT) Log.v(TAG_EDIT, "closeIme escape north"); 470 // Escape north handling: stay on current item, but close editor 471 handled = true; 472 mGroup.fillAndStay(GuidedActionAdapter.this, v); 473 } 474 return handled; 475 } 476 477 @Override 478 public boolean onKeyPreIme(EditText editText, int keyCode, KeyEvent event) { 479 if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME key: " + keyCode); 480 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { 481 mGroup.fillAndStay(GuidedActionAdapter.this, editText); 482 return true; 483 } else if (keyCode == KeyEvent.KEYCODE_ENTER 484 && event.getAction() == KeyEvent.ACTION_UP) { 485 mGroup.fillAndGoNext(GuidedActionAdapter.this, editText); 486 return true; 487 } 488 return false; 489 } 490 491 } 492 493} 494