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