GuidedActionsStylist.java revision 11cb62de8bfc6b5b6d22811ad12a1e60451b82be
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.animation.Animator; 17import android.animation.AnimatorInflater; 18import android.animation.AnimatorListenerAdapter; 19import android.animation.AnimatorSet; 20import android.animation.ObjectAnimator; 21import android.content.Context; 22import android.content.pm.PackageManager; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.graphics.drawable.Drawable; 26import android.net.Uri; 27import android.support.annotation.NonNull; 28import android.support.v17.leanback.R; 29import android.support.v17.leanback.widget.VerticalGridView; 30import android.support.v4.content.ContextCompat; 31import android.support.v4.view.ViewCompat; 32import android.support.v7.widget.RecyclerView; 33import android.support.v7.widget.RecyclerView.ViewHolder; 34import android.text.TextUtils; 35import android.util.Log; 36import android.util.TypedValue; 37import android.view.animation.DecelerateInterpolator; 38import android.view.LayoutInflater; 39import android.view.View; 40import android.view.ViewGroup; 41import android.view.ViewGroup.LayoutParams; 42import android.view.ViewPropertyAnimator; 43import android.view.ViewTreeObserver; 44import android.view.WindowManager; 45import android.widget.Checkable; 46import android.widget.EditText; 47import android.widget.ImageView; 48import android.widget.TextView; 49 50import java.util.List; 51 52/** 53 * GuidedActionsStylist is used within a {@link android.support.v17.leanback.app.GuidedStepFragment} 54 * to supply the right-side panel where users can take actions. It consists of a container for the 55 * list of actions, and a stationary selector view that indicates visually the location of focus. 56 * <p> 57 * Many aspects of the base GuidedActionsStylist can be customized through theming; see the 58 * theme attributes below. Note that these attributes are not set on individual elements in layout 59 * XML, but instead would be set in a custom theme. See 60 * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a> 61 * for more information. 62 * <p> 63 * If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to 64 * override the {@link #onProvideLayoutId} method to change the layout used to display the 65 * list container and selector, or the {@link #onProvideItemLayoutId} method to change the layout 66 * used to display each action. 67 * <p> 68 * Note: If an alternate list layout is provided, the following view IDs must be supplied: 69 * <ul> 70 * <li>{@link android.support.v17.leanback.R.id#guidedactions_selector}</li> 71 * <li>{@link android.support.v17.leanback.R.id#guidedactions_list}</li> 72 * </ul><p> 73 * These view IDs must be present in order for the stylist to function. The list ID must correspond 74 * to a {@link VerticalGridView} or subclass. 75 * <p> 76 * If an alternate item layout is provided, the following view IDs should be used to refer to base 77 * elements: 78 * <ul> 79 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_content}</li> 80 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_title}</li> 81 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_description}</li> 82 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_icon}</li> 83 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_checkmark}</li> 84 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_chevron}</li> 85 * </ul><p> 86 * These view IDs are allowed to be missing, in which case the corresponding views in {@link 87 * GuidedActionsStylist.ViewHolder} will be null. 88 * <p> 89 * In order to support editable actions, the view associated with guidedactions_item_title should 90 * be a subclass of {@link android.widget.EditText}, and should satisfy the {@link 91 * ImeKeyMonitor} interface. 92 * 93 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeAppearingAnimation 94 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeDisappearingAnimation 95 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorShowAnimation 96 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorHideAnimation 97 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorStyle 98 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsListStyle 99 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContainerStyle 100 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemCheckmarkStyle 101 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemIconStyle 102 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContentStyle 103 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemTitleStyle 104 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemDescriptionStyle 105 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemChevronStyle 106 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionPressedAnimation 107 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUnpressedAnimation 108 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionEnabledChevronAlpha 109 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDisabledChevronAlpha 110 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMinLines 111 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMaxLines 112 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDescriptionMinLines 113 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionVerticalPadding 114 * @see android.R.styleable#Theme_listChoiceIndicatorSingle 115 * @see android.R.styleable#Theme_listChoiceIndicatorMultiple 116 * @see android.support.v17.leanback.app.GuidedStepFragment 117 * @see GuidedAction 118 */ 119public class GuidedActionsStylist implements FragmentAnimationProvider { 120 121 /** 122 * Default viewType that associated with default layout Id for the action item. 123 * @see #getItemViewType(GuidedAction) 124 * @see #onProvideItemLayoutId(int) 125 * @see #onCreateViewHolder(ViewGroup, int) 126 */ 127 public static final int VIEW_TYPE_DEFAULT = 0; 128 129 /** 130 * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link 131 * GuidedActionsStylist} may also wish to subclass this in order to add fields. 132 * @see GuidedAction 133 */ 134 public static class ViewHolder { 135 136 public final View view; 137 138 private View mContentView; 139 private TextView mTitleView; 140 private TextView mDescriptionView; 141 private ImageView mIconView; 142 private ImageView mCheckmarkView; 143 private ImageView mChevronView; 144 private boolean mInEditing; 145 private boolean mInEditingDescription; 146 147 /** 148 * Constructs an ViewHolder and caches the relevant subviews. 149 */ 150 public ViewHolder(View v) { 151 view = v; 152 153 mContentView = v.findViewById(R.id.guidedactions_item_content); 154 mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title); 155 mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description); 156 mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon); 157 mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark); 158 mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron); 159 } 160 161 /** 162 * Returns the content view within this view holder's view, where title and description are 163 * shown. 164 */ 165 public View getContentView() { 166 return mContentView; 167 } 168 169 /** 170 * Returns the title view within this view holder's view. 171 */ 172 public TextView getTitleView() { 173 return mTitleView; 174 } 175 176 /** 177 * Convenience method to return an editable version of the title, if possible, 178 * or null if the title view isn't an EditText. 179 */ 180 public EditText getEditableTitleView() { 181 return (mTitleView instanceof EditText) ? (EditText)mTitleView : null; 182 } 183 184 /** 185 * Returns the description view within this view holder's view. 186 */ 187 public TextView getDescriptionView() { 188 return mDescriptionView; 189 } 190 191 /** 192 * Convenience method to return an editable version of the description, if possible, 193 * or null if the description view isn't an EditText. 194 */ 195 public EditText getEditableDescriptionView() { 196 return (mDescriptionView instanceof EditText) ? (EditText)mDescriptionView : null; 197 } 198 199 /** 200 * Returns the icon view within this view holder's view. 201 */ 202 public ImageView getIconView() { 203 return mIconView; 204 } 205 206 /** 207 * Returns the checkmark view within this view holder's view. 208 */ 209 public ImageView getCheckmarkView() { 210 return mCheckmarkView; 211 } 212 213 /** 214 * Returns the chevron view within this view holder's view. 215 */ 216 public ImageView getChevronView() { 217 return mChevronView; 218 } 219 220 /** 221 * Returns true if the TextView is in editing title or description, false otherwise. 222 */ 223 public boolean isInEditing() { 224 return mInEditing; 225 } 226 227 /** 228 * Returns true if the TextView is in editing description, false otherwise. 229 */ 230 public boolean isInEditingDescription() { 231 return mInEditingDescription; 232 } 233 234 public View getEditingView() { 235 if (mInEditing) { 236 return mInEditingDescription ? mDescriptionView : mTitleView; 237 } else { 238 return null; 239 } 240 } 241 } 242 243 private static String TAG = "GuidedActionsStylist"; 244 245 private View mMainView; 246 private VerticalGridView mActionsGridView; 247 private View mBgView; 248 private View mSelectorView; 249 private boolean mButtonActions; 250 251 // Cached values from resources 252 private float mEnabledTextAlpha; 253 private float mDisabledTextAlpha; 254 private float mEnabledDescriptionAlpha; 255 private float mDisabledDescriptionAlpha; 256 private float mEnabledChevronAlpha; 257 private float mDisabledChevronAlpha; 258 private int mTitleMinLines; 259 private int mTitleMaxLines; 260 private int mDescriptionMinLines; 261 private int mVerticalPadding; 262 private int mDisplayHeight; 263 264 /** 265 * Creates a view appropriate for displaying a list of GuidedActions, using the provided 266 * inflater and container. 267 * <p> 268 * <i>Note: Does not actually add the created view to the container; the caller should do 269 * this.</i> 270 * @param inflater The layout inflater to be used when constructing the view. 271 * @param container The view group to be passed in the call to 272 * <code>LayoutInflater.inflate</code>. 273 * @return The view to be added to the caller's view hierarchy. 274 */ 275 public View onCreateView(LayoutInflater inflater, ViewGroup container) { 276 mMainView = inflater.inflate(onProvideLayoutId(), container, false); 277 mSelectorView = mMainView.findViewById(R.id.guidedactions_selector); 278 mSelectorView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 279 @Override 280 public void onLayoutChange(View v, int left, int top, int right, int bottom, 281 int oldLeft, int oldTop, int oldRight, int oldBottom) { 282 final View focusedChild = mActionsGridView.getFocusedChild(); 283 if (focusedChild != null && mSelectorView.getVisibility() == View.VISIBLE && 284 mSelectorView.getHeight() > 0) { 285 mSelectorView.setScaleY((float) focusedChild.getHeight() 286 / mSelectorView.getHeight()); 287 } 288 } 289 }); 290 mBgView = mMainView.findViewById(R.id.guided_button_actions_background); 291 if (mMainView instanceof VerticalGridView) { 292 mActionsGridView = (VerticalGridView) mMainView; 293 } else { 294 mActionsGridView = (VerticalGridView) mMainView.findViewById(R.id.guidedactions_list); 295 if (mActionsGridView == null) { 296 throw new IllegalStateException("No ListView exists."); 297 } 298 mActionsGridView.setWindowAlignmentOffset(0); 299 mActionsGridView.setWindowAlignmentOffsetPercent(50f); 300 mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 301 if (mSelectorView != null) { 302 mActionsGridView.setOnScrollListener(new 303 SelectorAnimator(mSelectorView, mActionsGridView)); 304 } 305 } 306 307 if (mSelectorView != null) { 308 // ALlow focus to move to other views 309 mActionsGridView.getViewTreeObserver().addOnGlobalFocusChangeListener( 310 mGlobalFocusChangeListener); 311 } 312 313 // Cache widths, chevron alpha values, max and min text lines, etc 314 Context ctx = mMainView.getContext(); 315 TypedValue val = new TypedValue(); 316 mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha); 317 mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha); 318 mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines); 319 mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines); 320 mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines); 321 mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding); 322 mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)) 323 .getDefaultDisplay().getHeight(); 324 325 mEnabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string 326 .lb_guidedactions_item_unselected_text_alpha)); 327 mDisabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string 328 .lb_guidedactions_item_disabled_text_alpha)); 329 mEnabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string 330 .lb_guidedactions_item_unselected_description_text_alpha)); 331 mDisabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string 332 .lb_guidedactions_item_disabled_description_text_alpha)); 333 return mMainView; 334 } 335 336 /** 337 * Default implementation turns on background for actions and applies different Ids to views so 338 * that GuidedStepFragment could run transitions against two action lists. The method is called 339 * by GuidedStepFragment, app may override this function when replacing default layout file 340 * provided by {@link #onProvideLayoutId()} 341 */ 342 public void setAsButtonActions() { 343 mButtonActions = true; 344 mMainView.setId(R.id.guidedactions_root2); 345 ViewCompat.setTransitionName(mMainView, "guidedactions_root"); 346 mActionsGridView.setId(R.id.guidedactions_list2); 347 mSelectorView.setId(R.id.guidedactions_selector2); 348 ViewCompat.setTransitionName(mSelectorView, "guidedactions_selector2"); 349 mBgView.setId(R.id.guided_button_actions_background2); 350 ViewCompat.setTransitionName(mBgView, "guided_button_actions_background2"); 351 mBgView.setVisibility(View.VISIBLE); 352 } 353 354 /** 355 * Returns true if {@link #setAsButtonActions()} was called, false otherwise. 356 * @return True if {@link #setAsButtonActions()} was called, false otherwise. 357 */ 358 public boolean isButtonActions() { 359 return mButtonActions; 360 } 361 362 final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener = 363 new ViewTreeObserver.OnGlobalFocusChangeListener() { 364 private boolean mChildFocused; 365 366 @Override 367 public void onGlobalFocusChanged(View oldFocus, View newFocus) { 368 final View focusedChild = mActionsGridView.getFocusedChild(); 369 if (focusedChild == null) { 370 mSelectorView.setVisibility(View.INVISIBLE); 371 mChildFocused = false; 372 } else if (!mChildFocused) { 373 mChildFocused = true; 374 mSelectorView.setVisibility(View.VISIBLE); 375 if (mSelectorView.getHeight() > 0) { 376 mSelectorView.setScaleY((float) focusedChild.getHeight() 377 / mSelectorView.getHeight()); 378 } 379 } 380 } 381 }; 382 383 /** 384 * Called when destroy the View created by GuidedActionsStylist. 385 */ 386 public void onDestroyView() { 387 if (mSelectorView != null) { 388 mActionsGridView.getViewTreeObserver().removeOnGlobalFocusChangeListener( 389 mGlobalFocusChangeListener); 390 } 391 mActionsGridView = null; 392 mSelectorView = null; 393 mBgView = null; 394 mMainView = null; 395 } 396 397 /** 398 * Returns the VerticalGridView that displays the list of GuidedActions. 399 * @return The VerticalGridView for this presenter. 400 */ 401 public VerticalGridView getActionsGridView() { 402 return mActionsGridView; 403 } 404 405 /** 406 * Provides the resource ID of the layout defining the host view for the list of guided actions. 407 * Subclasses may override to provide their own customized layouts. The base implementation 408 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions}. If overridden, the 409 * substituted layout should contain matching IDs for any views that should be managed by the 410 * base class; this can be achieved by starting with a copy of the base layout file. 411 * @return The resource ID of the layout to be inflated to define the host view for the list 412 * of GuidedActions. 413 */ 414 public int onProvideLayoutId() { 415 return R.layout.lb_guidedactions; 416 } 417 418 /** 419 * Return view type of action, each different type can have differently associated layout Id. 420 * Default implementation returns {@link #VIEW_TYPE_DEFAULT}. 421 * @param action The action object. 422 * @return View type that used in {@link #onProvideItemLayoutId(int)}. 423 */ 424 public int getItemViewType(GuidedAction action) { 425 return VIEW_TYPE_DEFAULT; 426 } 427 428 /** 429 * Provides the resource ID of the layout defining the view for an individual guided actions. 430 * Subclasses may override to provide their own customized layouts. The base implementation 431 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden, 432 * the substituted layout should contain matching IDs for any views that should be managed by 433 * the base class; this can be achieved by starting with a copy of the base layout file. Note 434 * that in order for the item to support editing, the title view should both subclass {@link 435 * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link 436 * GuidedActionEditText}. To support different types of Layouts, override {@link 437 * #onProvideItemLayoutId(int)}. 438 * @return The resource ID of the layout to be inflated to define the view to display an 439 * individual GuidedAction. 440 */ 441 public int onProvideItemLayoutId() { 442 return R.layout.lb_guidedactions_item; 443 } 444 445 /** 446 * Provides the resource ID of the layout defining the view for an individual guided actions. 447 * Subclasses may override to provide their own customized layouts. The base implementation 448 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden, 449 * the substituted layout should contain matching IDs for any views that should be managed by 450 * the base class; this can be achieved by starting with a copy of the base layout file. Note 451 * that in order for the item to support editing, the title view should both subclass {@link 452 * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link 453 * GuidedActionEditText}. 454 * @param viewType View type returned by {@link #getItemViewType(GuidedAction)} 455 * @return The resource ID of the layout to be inflated to define the view to display an 456 * individual GuidedAction. 457 */ 458 public int onProvideItemLayoutId(int viewType) { 459 if (viewType == VIEW_TYPE_DEFAULT) { 460 return onProvideItemLayoutId(); 461 } else { 462 throw new RuntimeException("ViewType " + viewType + 463 " not supported in GuidedActionsStylist"); 464 } 465 } 466 467 /** 468 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 469 * may choose to return a subclass of ViewHolder. To support different view types, override 470 * {@link #onCreateViewHolder(ViewGroup, int)} 471 * <p> 472 * <i>Note: Should not actually add the created view to the parent; the caller will do 473 * this.</i> 474 * @param parent The view group to be used as the parent of the new view. 475 * @return The view to be added to the caller's view hierarchy. 476 */ 477 public ViewHolder onCreateViewHolder(ViewGroup parent) { 478 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 479 View v = inflater.inflate(onProvideItemLayoutId(), parent, false); 480 return new ViewHolder(v); 481 } 482 483 /** 484 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 485 * may choose to return a subclass of ViewHolder. 486 * <p> 487 * <i>Note: Should not actually add the created view to the parent; the caller will do 488 * this.</i> 489 * @param parent The view group to be used as the parent of the new view. 490 * @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)} 491 * @return The view to be added to the caller's view hierarchy. 492 */ 493 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 494 if (viewType == VIEW_TYPE_DEFAULT) { 495 return onCreateViewHolder(parent); 496 } 497 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 498 View v = inflater.inflate(onProvideItemLayoutId(viewType), parent, false); 499 return new ViewHolder(v); 500 } 501 502 /** 503 * Binds a {@link ViewHolder} to a particular {@link GuidedAction}. 504 * @param vh The view holder to be associated with the given action. 505 * @param action The guided action to be displayed by the view holder's view. 506 * @return The view to be added to the caller's view hierarchy. 507 */ 508 public void onBindViewHolder(ViewHolder vh, GuidedAction action) { 509 510 if (vh.mTitleView != null) { 511 vh.mTitleView.setText(action.getTitle()); 512 vh.mTitleView.setAlpha(action.isEnabled() ? mEnabledTextAlpha : mDisabledTextAlpha); 513 vh.mTitleView.setFocusable(action.isEditable()); 514 } 515 if (vh.mDescriptionView != null) { 516 vh.mDescriptionView.setText(action.getDescription()); 517 vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ? 518 View.GONE : View.VISIBLE); 519 vh.mDescriptionView.setAlpha(action.isEnabled() ? mEnabledDescriptionAlpha : 520 mDisabledDescriptionAlpha); 521 vh.mDescriptionView.setFocusable(action.isDescriptionEditable()); 522 } 523 // Clients might want the check mark view to be gone entirely, in which case, ignore it. 524 if (vh.mCheckmarkView != null) { 525 onBindCheckMarkView(vh, action); 526 } 527 528 if (vh.mChevronView != null) { 529 onBindChevronView(vh, action); 530 } 531 532 if (action.hasMultilineDescription()) { 533 if (vh.mTitleView != null) { 534 vh.mTitleView.setMaxLines(mTitleMaxLines); 535 if (vh.mDescriptionView != null) { 536 vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(vh.view.getContext(), 537 vh.mTitleView)); 538 } 539 } 540 } else { 541 if (vh.mTitleView != null) { 542 vh.mTitleView.setMaxLines(mTitleMinLines); 543 } 544 if (vh.mDescriptionView != null) { 545 vh.mDescriptionView.setMaxLines(mDescriptionMinLines); 546 } 547 } 548 setEditingMode(vh, action, false); 549 if (action.isFocusable()) { 550 vh.view.setFocusable(true); 551 if (vh.view instanceof ViewGroup) { 552 ((ViewGroup) vh.view).setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 553 } 554 } else { 555 vh.view.setFocusable(false); 556 if (vh.view instanceof ViewGroup) { 557 ((ViewGroup) vh.view).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 558 } 559 } 560 } 561 562 public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) { 563 if (editing != vh.mInEditing) { 564 vh.mInEditing = editing; 565 onEditingModeChange(vh, action, editing); 566 } 567 } 568 569 protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) { 570 TextView titleView = vh.getTitleView(); 571 TextView descriptionView = vh.getDescriptionView(); 572 if (editing) { 573 CharSequence editTitle = action.getEditTitle(); 574 if (titleView != null && editTitle != null) { 575 titleView.setText(editTitle); 576 } 577 CharSequence editDescription = action.getEditDescription(); 578 if (descriptionView != null && editDescription != null) { 579 descriptionView.setText(editDescription); 580 } 581 if (action.isDescriptionEditable()) { 582 if (descriptionView != null) { 583 descriptionView.setVisibility(View.VISIBLE); 584 descriptionView.setInputType(action.getDescriptionEditInputType()); 585 } 586 vh.mInEditingDescription = true; 587 } else { 588 vh.mInEditingDescription = false; 589 if (titleView != null) { 590 titleView.setInputType(action.getEditInputType()); 591 } 592 } 593 } else { 594 if (titleView != null) { 595 titleView.setText(action.getTitle()); 596 } 597 if (descriptionView != null) { 598 descriptionView.setText(action.getDescription()); 599 } 600 if (vh.mInEditingDescription) { 601 if (descriptionView != null) { 602 descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ? 603 View.GONE : View.VISIBLE); 604 descriptionView.setInputType(action.getDescriptionInputType()); 605 } 606 vh.mInEditingDescription = false; 607 } else { 608 if (titleView != null) { 609 titleView.setInputType(action.getInputType()); 610 } 611 } 612 } 613 } 614 615 /** 616 * Animates the view holder's view (or subviews thereof) when the action has had its focus 617 * state changed. 618 * @param vh The view holder associated with the relevant action. 619 * @param focused True if the action has become focused, false if it has lost focus. 620 */ 621 public void onAnimateItemFocused(ViewHolder vh, boolean focused) { 622 // No animations for this, currently, because the animation is done on 623 // mSelectorView 624 } 625 626 /** 627 * Animates the view holder's view (or subviews thereof) when the action has had its press 628 * state changed. 629 * @param vh The view holder associated with the relevant action. 630 * @param pressed True if the action has been pressed, false if it has been unpressed. 631 */ 632 public void onAnimateItemPressed(ViewHolder vh, boolean pressed) { 633 int attr = pressed ? R.attr.guidedActionPressedAnimation : 634 R.attr.guidedActionUnpressedAnimation; 635 createAnimator(vh.view, attr).start(); 636 } 637 638 /** 639 * Resets the view holder's view to unpressed state. 640 * @param vh The view holder associated with the relevant action. 641 */ 642 public void onAnimateItemPressedCancelled(ViewHolder vh) { 643 createAnimator(vh.view, R.attr.guidedActionUnpressedAnimation).end(); 644 } 645 646 /** 647 * Animates the view holder's view (or subviews thereof) when the action has had its check state 648 * changed. Default implementation calls setChecked() if {@link ViewHolder#getCheckmarkView()} 649 * is instance of {@link Checkable}. 650 * 651 * @param vh The view holder associated with the relevant action. 652 * @param checked True if the action has become checked, false if it has become unchecked. 653 * @see #onBindCheckMarkView(ViewHolder, GuidedAction) 654 */ 655 public void onAnimateItemChecked(ViewHolder vh, boolean checked) { 656 if (vh.mCheckmarkView instanceof Checkable) { 657 ((Checkable) vh.mCheckmarkView).setChecked(checked); 658 } 659 } 660 661 /** 662 * Sets states of check mark view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)} 663 * when action's checkset Id is other than {@link GuidedAction#NO_CHECK_SET}. Default 664 * implementation assigns drawable loaded from theme attribute 665 * {@link android.R.attr#listChoiceIndicatorMultiple} for checkbox or 666 * {@link android.R.attr#listChoiceIndicatorSingle} for radio button. Subclass rarely needs 667 * override the method, instead app can provide its own drawable that supports transition 668 * animations, change theme attributes {@link android.R.attr#listChoiceIndicatorMultiple} and 669 * {@link android.R.attr#listChoiceIndicatorSingle} in {android.support.v17.leanback.R. 670 * styleable#LeanbackGuidedStepTheme}. 671 * 672 * @param vh The view holder associated with the relevant action. 673 * @param action The GuidedAction object to bind to. 674 * @see #onAnimateItemChecked(ViewHolder, boolean) 675 */ 676 public void onBindCheckMarkView(ViewHolder vh, GuidedAction action) { 677 if (action.getCheckSetId() != GuidedAction.NO_CHECK_SET) { 678 vh.mCheckmarkView.setVisibility(View.VISIBLE); 679 int attrId = action.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID ? 680 android.R.attr.listChoiceIndicatorMultiple : 681 android.R.attr.listChoiceIndicatorSingle; 682 final Context context = vh.mCheckmarkView.getContext(); 683 Drawable drawable = null; 684 TypedValue typedValue = new TypedValue(); 685 if (context.getTheme().resolveAttribute(attrId, typedValue, true)) { 686 drawable = ContextCompat.getDrawable(context, typedValue.resourceId); 687 } 688 vh.mCheckmarkView.setImageDrawable(drawable); 689 if (vh.mCheckmarkView instanceof Checkable) { 690 ((Checkable) vh.mCheckmarkView).setChecked(action.isChecked()); 691 } 692 } else { 693 vh.mCheckmarkView.setVisibility(View.GONE); 694 } 695 } 696 697 /** 698 * Sets states of chevron view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)}. 699 * Subclass may override. 700 * 701 * @param vh The view holder associated with the relevant action. 702 * @param action The GuidedAction object to bind to. 703 */ 704 public void onBindChevronView(ViewHolder vh, GuidedAction action) { 705 vh.mChevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.GONE); 706 vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha : 707 mDisabledChevronAlpha); 708 } 709 710 /* 711 * ========================================== 712 * FragmentAnimationProvider overrides 713 * ========================================== 714 */ 715 716 /** 717 * {@inheritDoc} 718 */ 719 @Override 720 public void onImeAppearing(@NonNull List<Animator> animators) { 721 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepImeAppearingAnimation)); 722 animators.add(createAnimator(mSelectorView, R.attr.guidedStepImeAppearingAnimation)); 723 } 724 725 /** 726 * {@inheritDoc} 727 */ 728 @Override 729 public void onImeDisappearing(@NonNull List<Animator> animators) { 730 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepImeDisappearingAnimation)); 731 animators.add(createAnimator(mSelectorView, R.attr.guidedStepImeDisappearingAnimation)); 732 } 733 734 /* 735 * ========================================== 736 * Private methods 737 * ========================================== 738 */ 739 740 private float getFloat(Context ctx, TypedValue typedValue, int attrId) { 741 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 742 // Android resources don't have a native float type, so we have to use strings. 743 return Float.valueOf(ctx.getResources().getString(typedValue.resourceId)); 744 } 745 746 private int getInteger(Context ctx, TypedValue typedValue, int attrId) { 747 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 748 return ctx.getResources().getInteger(typedValue.resourceId); 749 } 750 751 private int getDimension(Context ctx, TypedValue typedValue, int attrId) { 752 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 753 return ctx.getResources().getDimensionPixelSize(typedValue.resourceId); 754 } 755 756 private static Animator createAnimator(View v, int attrId) { 757 Context ctx = v.getContext(); 758 TypedValue typedValue = new TypedValue(); 759 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 760 Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId); 761 animator.setTarget(v); 762 return animator; 763 } 764 765 private boolean setIcon(final ImageView iconView, GuidedAction action) { 766 Drawable icon = null; 767 if (iconView != null) { 768 Context context = iconView.getContext(); 769 icon = action.getIcon(); 770 if (icon != null) { 771 // setImageDrawable resets the drawable's level unless we set the view level first. 772 iconView.setImageLevel(icon.getLevel()); 773 iconView.setImageDrawable(icon); 774 iconView.setVisibility(View.VISIBLE); 775 } else { 776 iconView.setVisibility(View.GONE); 777 } 778 } 779 return icon != null; 780 } 781 782 /** 783 * @return the max height in pixels the description can be such that the 784 * action nicely takes up the entire screen. 785 */ 786 private int getDescriptionMaxHeight(Context context, TextView title) { 787 // The 2 multiplier on the title height calculation is a 788 // conservative estimate for font padding which can not be 789 // calculated at this stage since the view hasn't been rendered yet. 790 return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight()); 791 } 792 793 /** 794 * SelectorAnimator 795 * Controls animation for selected item backgrounds 796 * TODO: Move into focus animation override? 797 */ 798 private static class SelectorAnimator extends RecyclerView.OnScrollListener { 799 800 private final View mSelectorView; 801 private final ViewGroup mParentView; 802 private volatile boolean mFadedOut = true; 803 804 SelectorAnimator(View selectorView, ViewGroup parentView) { 805 mSelectorView = selectorView; 806 mParentView = parentView; 807 } 808 809 // We want to fade in the selector if we've stopped scrolling on it. If 810 // we're scrolling, we want to ensure to dim the selector if we haven't 811 // already. We dim the last highlighted view so that while a user is 812 // scrolling, nothing is highlighted. 813 @Override 814 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 815 Animator animator = null; 816 boolean fadingOut = false; 817 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 818 // The selector starts with a height of 0. In order to scale up from 819 // 0, we first need the set the height to 1 and scale from there. 820 View focusedChild = mParentView.getFocusedChild(); 821 if (focusedChild != null) { 822 int selectorHeight = mSelectorView.getHeight(); 823 float scaleY = (float) focusedChild.getHeight() / selectorHeight; 824 AnimatorSet animators = (AnimatorSet)createAnimator(mSelectorView, 825 R.attr.guidedActionsSelectorShowAnimation); 826 if (mFadedOut) { 827 // selector is completely faded out, so we can just scale before fading in. 828 mSelectorView.setScaleY(scaleY); 829 animator = animators.getChildAnimations().get(0); 830 } else { 831 // selector is not faded out, so we must animate the scale as we fade in. 832 ((ObjectAnimator)animators.getChildAnimations().get(1)) 833 .setFloatValues(scaleY); 834 animator = animators; 835 } 836 } 837 } else { 838 animator = createAnimator(mSelectorView, R.attr.guidedActionsSelectorHideAnimation); 839 fadingOut = true; 840 } 841 if (animator != null) { 842 animator.addListener(new Listener(fadingOut)); 843 animator.start(); 844 } 845 } 846 847 /** 848 * Sets {@link BaseScrollAdapterFragment#mFadedOut} 849 * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff 850 * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0 851 * (faded out). If false the view either has an alpha of 1 (visible) or 852 * is in the process of animating. 853 */ 854 private class Listener implements Animator.AnimatorListener { 855 private boolean mFadingOut; 856 private boolean mCanceled; 857 858 public Listener(boolean fadingOut) { 859 mFadingOut = fadingOut; 860 } 861 862 @Override 863 public void onAnimationStart(Animator animation) { 864 if (!mFadingOut) { 865 mFadedOut = false; 866 } 867 } 868 869 @Override 870 public void onAnimationEnd(Animator animation) { 871 if (!mCanceled && mFadingOut) { 872 mFadedOut = true; 873 } 874 } 875 876 @Override 877 public void onAnimationCancel(Animator animation) { 878 mCanceled = true; 879 } 880 881 @Override 882 public void onAnimationRepeat(Animator animation) { 883 } 884 } 885 } 886 887} 888