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 android.content.Context; 17import android.database.DataSetObserver; 18import android.media.AudioManager; 19import android.support.v17.leanback.R; 20import android.support.v7.widget.RecyclerView; 21import android.support.v7.widget.RecyclerView.ViewHolder; 22import android.util.Log; 23import android.view.KeyEvent; 24import android.view.LayoutInflater; 25import android.view.View; 26import android.view.ViewGroup; 27import android.view.ViewParent; 28import android.view.inputmethod.EditorInfo; 29import android.view.inputmethod.InputMethodManager; 30import android.widget.AdapterView.OnItemSelectedListener; 31import android.widget.EditText; 32import android.widget.ImageView; 33import android.widget.TextView; 34import android.widget.TextView.OnEditorActionListener; 35 36import java.util.ArrayList; 37import java.util.List; 38 39/** 40 * GuidedActionAdapter instantiates views for guided actions, and manages their interactions. 41 * Presentation (view creation and state animation) is delegated to a {@link 42 * GuidedActionsStylist}, while clients are notified of interactions via 43 * {@link GuidedActionAdapter.ClickListener} and {@link GuidedActionAdapter.FocusListener}. 44 * @hide 45 */ 46public class GuidedActionAdapter extends RecyclerView.Adapter { 47 private static final String TAG = "GuidedActionAdapter"; 48 private static final boolean DEBUG = false; 49 50 private static final String TAG_EDIT = "EditableAction"; 51 private static final boolean DEBUG_EDIT = false; 52 53 /** 54 * Object listening for click events within a {@link GuidedActionAdapter}. 55 */ 56 public interface ClickListener { 57 58 /** 59 * Called when the user clicks on an action. 60 */ 61 public void onGuidedActionClicked(GuidedAction action); 62 63 } 64 65 /** 66 * Object listening for focus events within a {@link GuidedActionAdapter}. 67 */ 68 public interface FocusListener { 69 70 /** 71 * Called when the user focuses on an action. 72 */ 73 public void onGuidedActionFocused(GuidedAction action); 74 } 75 76 /** 77 * Object listening for edit events within a {@link GuidedActionAdapter}. 78 */ 79 public interface EditListener { 80 81 /** 82 * Called when the user exits edit mode on an action. 83 */ 84 public void onGuidedActionEditCanceled(GuidedAction action); 85 86 /** 87 * Called when the user exits edit mode on an action and process confirm button in IME. 88 */ 89 public long onGuidedActionEditedAndProceed(GuidedAction action); 90 91 /** 92 * Called when Ime Open 93 */ 94 public void onImeOpen(); 95 96 /** 97 * Called when Ime Close 98 */ 99 public void onImeClose(); 100 } 101 102 private final boolean mIsSubAdapter; 103 private final ActionOnKeyListener mActionOnKeyListener; 104 private final ActionOnFocusListener mActionOnFocusListener; 105 private final ActionEditListener mActionEditListener; 106 private final List<GuidedAction> mActions; 107 private ClickListener mClickListener; 108 private final GuidedActionsStylist mStylist; 109 private final View.OnClickListener mOnClickListener = new View.OnClickListener() { 110 @Override 111 public void onClick(View v) { 112 if (v != null && v.getWindowToken() != null && getRecyclerView() != null) { 113 GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder) 114 getRecyclerView().getChildViewHolder(v); 115 GuidedAction action = avh.getAction(); 116 if (action.hasTextEditable()) { 117 if (DEBUG_EDIT) Log.v(TAG_EDIT, "openIme by click"); 118 mGroup.openIme(GuidedActionAdapter.this, avh); 119 } else if (action.hasEditableActivatorView()) { 120 if (DEBUG_EDIT) Log.v(TAG_EDIT, "toggle editing mode by click"); 121 getGuidedActionsStylist().setEditingMode(avh, avh.getAction(), 122 !avh.isInEditingActivatorView()); 123 } else { 124 handleCheckedActions(avh); 125 if (action.isEnabled() && !action.infoOnly()) { 126 performOnActionClick(avh); 127 } 128 } 129 } 130 } 131 }; 132 GuidedActionAdapterGroup mGroup; 133 134 /** 135 * Constructs a GuidedActionAdapter with the given list of guided actions, the given click and 136 * focus listeners, and the given presenter. 137 * @param actions The list of guided actions this adapter will manage. 138 * @param focusListener The focus listener for items in this adapter. 139 * @param presenter The presenter that will manage the display of items in this adapter. 140 */ 141 public GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener, 142 FocusListener focusListener, GuidedActionsStylist presenter, boolean isSubAdapter) { 143 super(); 144 mActions = actions == null ? new ArrayList<GuidedAction>() : 145 new ArrayList<GuidedAction>(actions); 146 mClickListener = clickListener; 147 mStylist = presenter; 148 mActionOnKeyListener = new ActionOnKeyListener(); 149 mActionOnFocusListener = new ActionOnFocusListener(focusListener); 150 mActionEditListener = new ActionEditListener(); 151 mIsSubAdapter = isSubAdapter; 152 } 153 154 /** 155 * Sets the list of actions managed by this adapter. 156 * @param actions The list of actions to be managed. 157 */ 158 public void setActions(List<GuidedAction> actions) { 159 mActionOnFocusListener.unFocus(); 160 mActions.clear(); 161 mActions.addAll(actions); 162 notifyDataSetChanged(); 163 } 164 165 /** 166 * Returns the count of actions managed by this adapter. 167 * @return The count of actions managed by this adapter. 168 */ 169 public int getCount() { 170 return mActions.size(); 171 } 172 173 /** 174 * Returns the GuidedAction at the given position in the managed list. 175 * @param position The position of the desired GuidedAction. 176 * @return The GuidedAction at the given position. 177 */ 178 public GuidedAction getItem(int position) { 179 return mActions.get(position); 180 } 181 182 /** 183 * Return index of action in array 184 * @param action Action to search index. 185 * @return Index of Action in array. 186 */ 187 public int indexOf(GuidedAction action) { 188 return mActions.indexOf(action); 189 } 190 191 /** 192 * @return GuidedActionsStylist used to build the actions list UI. 193 */ 194 public GuidedActionsStylist getGuidedActionsStylist() { 195 return mStylist; 196 } 197 198 /** 199 * Sets the click listener for items managed by this adapter. 200 * @param clickListener The click listener for this adapter. 201 */ 202 public void setClickListener(ClickListener clickListener) { 203 mClickListener = clickListener; 204 } 205 206 /** 207 * Sets the focus listener for items managed by this adapter. 208 * @param focusListener The focus listener for this adapter. 209 */ 210 public void setFocusListener(FocusListener focusListener) { 211 mActionOnFocusListener.setFocusListener(focusListener); 212 } 213 214 /** 215 * Used for serialization only. 216 * @hide 217 */ 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 private 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 /** 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 @Override 458 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 459 if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME action: " + actionId); 460 boolean handled = false; 461 if (actionId == EditorInfo.IME_ACTION_NEXT || 462 actionId == EditorInfo.IME_ACTION_DONE) { 463 mGroup.fillAndGoNext(GuidedActionAdapter.this, v); 464 handled = true; 465 } else if (actionId == EditorInfo.IME_ACTION_NONE) { 466 if (DEBUG_EDIT) Log.v(TAG_EDIT, "closeIme escape north"); 467 // Escape north handling: stay on current item, but close editor 468 handled = true; 469 mGroup.fillAndStay(GuidedActionAdapter.this, v); 470 } 471 return handled; 472 } 473 474 @Override 475 public boolean onKeyPreIme(EditText editText, int keyCode, KeyEvent event) { 476 if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME key: " + keyCode); 477 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { 478 mGroup.fillAndStay(GuidedActionAdapter.this, editText); 479 return true; 480 } else if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == 481 KeyEvent.ACTION_UP) { 482 mGroup.fillAndGoNext(GuidedActionAdapter.this, editText); 483 return true; 484 } 485 return false; 486 } 487 488 } 489 490} 491