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