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