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