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 */ 14 package android.support.v17.leanback.widget; 15 16 import android.animation.Animator; 17 import android.animation.AnimatorInflater; 18 import android.animation.AnimatorListenerAdapter; 19 import android.animation.AnimatorSet; 20 import android.animation.ObjectAnimator; 21 import android.content.Context; 22 import android.content.pm.PackageManager; 23 import android.content.res.Resources; 24 import android.content.res.TypedArray; 25 import android.graphics.drawable.Drawable; 26 import android.net.Uri; 27 import android.support.annotation.NonNull; 28 import android.support.v17.leanback.R; 29 import android.support.v17.leanback.widget.VerticalGridView; 30 import android.support.v7.widget.RecyclerView; 31 import android.support.v7.widget.RecyclerView.ViewHolder; 32 import android.text.TextUtils; 33 import android.util.Log; 34 import android.util.TypedValue; 35 import android.view.animation.DecelerateInterpolator; 36 import android.view.LayoutInflater; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.view.ViewGroup.LayoutParams; 40 import android.view.ViewPropertyAnimator; 41 import android.view.ViewTreeObserver; 42 import android.view.WindowManager; 43 import android.widget.ImageView; 44 import android.widget.TextView; 45 46 import java.util.List; 47 48 /** 49 * GuidedActionsStylist is used within a {@link android.support.v17.leanback.app.GuidedStepFragment} 50 * to supply the right-side panel where users can take actions. It consists of a container for the 51 * list of actions, and a stationary selector view that indicates visually the location of focus. 52 * <p> 53 * Many aspects of the base GuidedActionsStylist can be customized through theming; see the 54 * theme attributes below. Note that these attributes are not set on individual elements in layout 55 * XML, but instead would be set in a custom theme. See 56 * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a> 57 * for more information. 58 * <p> 59 * If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to 60 * override the {@link #onProvideLayoutId} method to change the layout used to display the 61 * list container and selector, or the {@link #onProvideItemLayoutId} method to change the layout 62 * used to display each action. 63 * <p> 64 * Note: If an alternate list layout is provided, the following view IDs must be supplied: 65 * <ul> 66 * <li>{@link android.support.v17.leanback.R.id#guidedactions_selector}</li> 67 * <li>{@link android.support.v17.leanback.R.id#guidedactions_list}</li> 68 * </ul><p> 69 * These view IDs must be present in order for the stylist to function. The list ID must correspond 70 * to a {@link VerticalGridView} or subclass. 71 * <p> 72 * If an alternate item layout is provided, the following view IDs should be used to refer to base 73 * elements: 74 * <ul> 75 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_content}</li> 76 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_title}</li> 77 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_description}</li> 78 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_icon}</li> 79 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_checkmark}</li> 80 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_chevron}</li> 81 * </ul><p> 82 * These view IDs are allowed to be missing, in which case the corresponding views in {@link 83 * GuidedActionsStylist.ViewHolder} will be null. 84 * 85 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsEntryAnimation 86 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorShowAnimation 87 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorHideAnimation 88 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsContainerStyle 89 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorStyle 90 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsListStyle 91 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContainerStyle 92 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemCheckmarkStyle 93 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemIconStyle 94 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContentStyle 95 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemTitleStyle 96 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemDescriptionStyle 97 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemChevronStyle 98 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionCheckedAnimation 99 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUncheckedAnimation 100 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionPressedAnimation 101 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUnpressedAnimation 102 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionEnabledChevronAlpha 103 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDisabledChevronAlpha 104 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidth 105 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthNoIcon 106 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMinLines 107 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMaxLines 108 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDescriptionMinLines 109 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionVerticalPadding 110 * @see android.support.v17.leanback.app.GuidedStepFragment 111 * @see GuidedAction 112 */ 113 public class GuidedActionsStylist implements FragmentAnimationProvider { 114 115 /** 116 * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link 117 * GuidedActionsStylist} may also wish to subclass this in order to add fields. 118 * @see GuidedAction 119 */ 120 public static class ViewHolder { 121 122 public final View view; 123 124 private View mContentView; 125 private TextView mTitleView; 126 private TextView mDescriptionView; 127 private ImageView mIconView; 128 private ImageView mCheckmarkView; 129 private ImageView mChevronView; 130 131 /** 132 * Constructs an ViewHolder and caches the relevant subviews. 133 */ ViewHolder(View v)134 public ViewHolder(View v) { 135 view = v; 136 137 mContentView = v.findViewById(R.id.guidedactions_item_content); 138 mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title); 139 mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description); 140 mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon); 141 mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark); 142 mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron); 143 } 144 145 /** 146 * Returns the content view within this view holder's view, where title and description are 147 * shown. 148 */ getContentView()149 public View getContentView() { 150 return mContentView; 151 } 152 153 /** 154 * Returns the title view within this view holder's view. 155 */ getTitleView()156 public TextView getTitleView() { 157 return mTitleView; 158 } 159 160 /** 161 * Returns the description view within this view holder's view. 162 */ getDescriptionView()163 public TextView getDescriptionView() { 164 return mDescriptionView; 165 } 166 167 /** 168 * Returns the icon view within this view holder's view. 169 */ getIconView()170 public ImageView getIconView() { 171 return mIconView; 172 } 173 174 /** 175 * Returns the checkmark view within this view holder's view. 176 */ getCheckmarkView()177 public ImageView getCheckmarkView() { 178 return mCheckmarkView; 179 } 180 181 /** 182 * Returns the chevron view within this view holder's view. 183 */ getChevronView()184 public ImageView getChevronView() { 185 return mChevronView; 186 } 187 188 } 189 190 private static String TAG = "GuidedActionsStylist"; 191 192 protected View mMainView; 193 protected VerticalGridView mActionsGridView; 194 protected View mSelectorView; 195 196 // Cached values from resources 197 private float mEnabledChevronAlpha; 198 private float mDisabledChevronAlpha; 199 private int mContentWidth; 200 private int mContentWidthNoIcon; 201 private int mTitleMinLines; 202 private int mTitleMaxLines; 203 private int mDescriptionMinLines; 204 private int mVerticalPadding; 205 private int mDisplayHeight; 206 207 /** 208 * Creates a view appropriate for displaying a list of GuidedActions, using the provided 209 * inflater and container. 210 * <p> 211 * <i>Note: Does not actually add the created view to the container; the caller should do 212 * this.</i> 213 * @param inflater The layout inflater to be used when constructing the view. 214 * @param container The view group to be passed in the call to 215 * <code>LayoutInflater.inflate</code>. 216 * @return The view to be added to the caller's view hierarchy. 217 */ onCreateView(LayoutInflater inflater, ViewGroup container)218 public View onCreateView(LayoutInflater inflater, ViewGroup container) { 219 mMainView = inflater.inflate(onProvideLayoutId(), container, false); 220 mSelectorView = mMainView.findViewById(R.id.guidedactions_selector); 221 if (mMainView instanceof VerticalGridView) { 222 mActionsGridView = (VerticalGridView) mMainView; 223 } else { 224 mActionsGridView = (VerticalGridView) mMainView.findViewById(R.id.guidedactions_list); 225 if (mActionsGridView == null) { 226 throw new IllegalStateException("No ListView exists."); 227 } 228 mActionsGridView.setWindowAlignmentOffset(0); 229 mActionsGridView.setWindowAlignmentOffsetPercent(50f); 230 mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 231 if (mSelectorView != null) { 232 mActionsGridView.setOnScrollListener(new 233 SelectorAnimator(mSelectorView, mActionsGridView)); 234 } 235 } 236 237 mActionsGridView.requestFocusFromTouch(); 238 239 if (mSelectorView != null) { 240 // ALlow focus to move to other views 241 mActionsGridView.getViewTreeObserver().addOnGlobalFocusChangeListener( 242 new ViewTreeObserver.OnGlobalFocusChangeListener() { 243 private boolean mChildFocused; 244 245 @Override 246 public void onGlobalFocusChanged(View oldFocus, View newFocus) { 247 View focusedChild = mActionsGridView.getFocusedChild(); 248 if (focusedChild == null) { 249 mSelectorView.setVisibility(View.INVISIBLE); 250 mChildFocused = false; 251 } else if (!mChildFocused) { 252 mChildFocused = true; 253 mSelectorView.setVisibility(View.VISIBLE); 254 updateSelectorView(focusedChild); 255 } 256 } 257 }); 258 } 259 260 // Cache widths, chevron alpha values, max and min text lines, etc 261 Context ctx = mMainView.getContext(); 262 TypedValue val = new TypedValue(); 263 mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha); 264 mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha); 265 mContentWidth = getDimension(ctx, val, R.attr.guidedActionContentWidth); 266 mContentWidthNoIcon = getDimension(ctx, val, R.attr.guidedActionContentWidthNoIcon); 267 mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines); 268 mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines); 269 mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines); 270 mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding); 271 mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)) 272 .getDefaultDisplay().getHeight(); 273 274 return mMainView; 275 } 276 277 /** 278 * Returns the VerticalGridView that displays the list of GuidedActions. 279 * @return The VerticalGridView for this presenter. 280 */ getActionsGridView()281 public VerticalGridView getActionsGridView() { 282 return mActionsGridView; 283 } 284 285 /** 286 * Provides the resource ID of the layout defining the host view for the list of guided actions. 287 * Subclasses may override to provide their own customized layouts. The base implementation 288 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions}. If overridden, the 289 * substituted layout should contain matching IDs for any views that should be managed by the 290 * base class; this can be achieved by starting with a copy of the base layout file. 291 * @return The resource ID of the layout to be inflated to define the host view for the list 292 * of GuidedActions. 293 */ onProvideLayoutId()294 public int onProvideLayoutId() { 295 return R.layout.lb_guidedactions; 296 } 297 298 /** 299 * Provides the resource ID of the layout defining the view for an individual guided actions. 300 * Subclasses may override to provide their own customized layouts. The base implementation 301 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden, 302 * the substituted layout should contain matching IDs for any views that should be managed by 303 * the base class; this can be achieved by starting with a copy of the base layout file. 304 * @return The resource ID of the layout to be inflated to define the view to display an 305 * individual GuidedAction. 306 */ onProvideItemLayoutId()307 public int onProvideItemLayoutId() { 308 return R.layout.lb_guidedactions_item; 309 } 310 311 /** 312 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 313 * may choose to return a subclass of ViewHolder. 314 * <p> 315 * <i>Note: Should not actually add the created view to the parent; the caller will do 316 * this.</i> 317 * @param parent The view group to be used as the parent of the new view. 318 * @return The view to be added to the caller's view hierarchy. 319 */ onCreateViewHolder(ViewGroup parent)320 public ViewHolder onCreateViewHolder(ViewGroup parent) { 321 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 322 View v = inflater.inflate(onProvideItemLayoutId(), parent, false); 323 return new ViewHolder(v); 324 } 325 326 /** 327 * Binds a {@link ViewHolder} to a particular {@link GuidedAction}. 328 * @param vh The view holder to be associated with the given action. 329 * @param action The guided action to be displayed by the view holder's view. 330 * @return The view to be added to the caller's view hierarchy. 331 */ onBindViewHolder(ViewHolder vh, GuidedAction action)332 public void onBindViewHolder(ViewHolder vh, GuidedAction action) { 333 334 if (vh.mTitleView != null) { 335 vh.mTitleView.setText(action.getTitle()); 336 } 337 if (vh.mDescriptionView != null) { 338 vh.mDescriptionView.setText(action.getDescription()); 339 vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ? 340 View.GONE : View.VISIBLE); 341 } 342 // Clients might want the check mark view to be gone entirely, in which case, ignore it. 343 if (vh.mCheckmarkView != null && vh.mCheckmarkView.getVisibility() != View.GONE) { 344 vh.mCheckmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE); 345 } 346 347 if (vh.mContentView != null) { 348 ViewGroup.LayoutParams contentLp = vh.mContentView.getLayoutParams(); 349 if (setIcon(vh.mIconView, action)) { 350 contentLp.width = mContentWidth; 351 } else { 352 contentLp.width = mContentWidthNoIcon; 353 } 354 vh.mContentView.setLayoutParams(contentLp); 355 } 356 357 if (vh.mChevronView != null) { 358 vh.mChevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE); 359 vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha : 360 mDisabledChevronAlpha); 361 } 362 363 if (action.hasMultilineDescription()) { 364 if (vh.mTitleView != null) { 365 vh.mTitleView.setMaxLines(mTitleMaxLines); 366 if (vh.mDescriptionView != null) { 367 vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(vh.view.getContext(), 368 vh.mTitleView)); 369 } 370 } 371 } else { 372 if (vh.mTitleView != null) { 373 vh.mTitleView.setMaxLines(mTitleMinLines); 374 } 375 if (vh.mDescriptionView != null) { 376 vh.mDescriptionView.setMaxLines(mDescriptionMinLines); 377 } 378 } 379 } 380 381 /** 382 * Animates the view holder's view (or subviews thereof) when the action has had its focus 383 * state changed. 384 * @param vh The view holder associated with the relevant action. 385 * @param focused True if the action has become focused, false if it has lost focus. 386 */ onAnimateItemFocused(ViewHolder vh, boolean focused)387 public void onAnimateItemFocused(ViewHolder vh, boolean focused) { 388 // No animations for this, currently, because the animation is done on 389 // mSelectorView 390 } 391 392 /** 393 * Animates the view holder's view (or subviews thereof) when the action has had its press 394 * state changed. 395 * @param vh The view holder associated with the relevant action. 396 * @param pressed True if the action has been pressed, false if it has been unpressed. 397 */ onAnimateItemPressed(ViewHolder vh, boolean pressed)398 public void onAnimateItemPressed(ViewHolder vh, boolean pressed) { 399 int attr = pressed ? R.attr.guidedActionPressedAnimation : 400 R.attr.guidedActionUnpressedAnimation; 401 createAnimator(vh.view, attr).start(); 402 } 403 404 /** 405 * Animates the view holder's view (or subviews thereof) when the action has had its check 406 * state changed. 407 * @param vh The view holder associated with the relevant action. 408 * @param checked True if the action has become checked, false if it has become unchecked. 409 */ onAnimateItemChecked(ViewHolder vh, boolean checked)410 public void onAnimateItemChecked(ViewHolder vh, boolean checked) { 411 final View checkView = vh.mCheckmarkView; 412 if (checkView != null) { 413 if (checked) { 414 checkView.setVisibility(View.VISIBLE); 415 createAnimator(checkView, R.attr.guidedActionCheckedAnimation).start(); 416 } else { 417 Animator animator = createAnimator(checkView, 418 R.attr.guidedActionUncheckedAnimation); 419 animator.addListener(new AnimatorListenerAdapter() { 420 @Override 421 public void onAnimationEnd(Animator animation) { 422 checkView.setVisibility(View.INVISIBLE); 423 } 424 }); 425 animator.start(); 426 } 427 } 428 } 429 430 /* 431 * ========================================== 432 * FragmentAnimationProvider overrides 433 * ========================================== 434 */ 435 436 /** 437 * {@inheritDoc} 438 */ 439 @Override onActivityEnter(@onNull List<Animator> animators)440 public void onActivityEnter(@NonNull List<Animator> animators) { 441 animators.add(createAnimator(mMainView, R.attr.guidedActionsEntryAnimation)); 442 } 443 444 /** 445 * {@inheritDoc} 446 */ 447 @Override onActivityExit(@onNull List<Animator> animators)448 public void onActivityExit(@NonNull List<Animator> animators) {} 449 450 /** 451 * {@inheritDoc} 452 */ 453 @Override onFragmentEnter(@onNull List<Animator> animators)454 public void onFragmentEnter(@NonNull List<Animator> animators) { 455 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepEntryAnimation)); 456 animators.add(createAnimator(mSelectorView, R.attr.guidedStepEntryAnimation)); 457 } 458 459 /** 460 * {@inheritDoc} 461 */ 462 @Override onFragmentExit(@onNull List<Animator> animators)463 public void onFragmentExit(@NonNull List<Animator> animators) { 464 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepExitAnimation)); 465 animators.add(createAnimator(mSelectorView, R.attr.guidedStepExitAnimation)); 466 } 467 468 /** 469 * {@inheritDoc} 470 */ 471 @Override onFragmentReenter(@onNull List<Animator> animators)472 public void onFragmentReenter(@NonNull List<Animator> animators) { 473 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepReentryAnimation)); 474 animators.add(createAnimator(mSelectorView, R.attr.guidedStepReentryAnimation)); 475 } 476 477 /** 478 * {@inheritDoc} 479 */ 480 @Override onFragmentReturn(@onNull List<Animator> animators)481 public void onFragmentReturn(@NonNull List<Animator> animators) { 482 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepReturnAnimation)); 483 animators.add(createAnimator(mSelectorView, R.attr.guidedStepReturnAnimation)); 484 } 485 486 /* 487 * ========================================== 488 * Private methods 489 * ========================================== 490 */ 491 updateSelectorView(View focusedChild)492 private void updateSelectorView(View focusedChild) { 493 // Display the selector view. 494 int height = focusedChild.getHeight(); 495 LayoutParams lp = mSelectorView.getLayoutParams(); 496 lp.height = height; 497 mSelectorView.setLayoutParams(lp); 498 mSelectorView.setAlpha(1f); 499 } 500 getFloat(Context ctx, TypedValue typedValue, int attrId)501 private float getFloat(Context ctx, TypedValue typedValue, int attrId) { 502 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 503 // Android resources don't have a native float type, so we have to use strings. 504 return Float.valueOf(ctx.getResources().getString(typedValue.resourceId)); 505 } 506 getInteger(Context ctx, TypedValue typedValue, int attrId)507 private int getInteger(Context ctx, TypedValue typedValue, int attrId) { 508 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 509 return ctx.getResources().getInteger(typedValue.resourceId); 510 } 511 getDimension(Context ctx, TypedValue typedValue, int attrId)512 private int getDimension(Context ctx, TypedValue typedValue, int attrId) { 513 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 514 return ctx.getResources().getDimensionPixelSize(typedValue.resourceId); 515 } 516 createAnimator(View v, int attrId)517 private static Animator createAnimator(View v, int attrId) { 518 Context ctx = v.getContext(); 519 TypedValue typedValue = new TypedValue(); 520 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 521 Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId); 522 animator.setTarget(v); 523 return animator; 524 } 525 setIcon(final ImageView iconView, GuidedAction action)526 private boolean setIcon(final ImageView iconView, GuidedAction action) { 527 Drawable icon = null; 528 if (iconView != null) { 529 Context context = iconView.getContext(); 530 icon = action.getIcon(); 531 if (icon != null) { 532 // setImageDrawable resets the drawable's level unless we set the view level first. 533 iconView.setImageLevel(icon.getLevel()); 534 iconView.setImageDrawable(icon); 535 iconView.setVisibility(View.VISIBLE); 536 } else { 537 iconView.setVisibility(View.GONE); 538 } 539 } 540 return icon != null; 541 } 542 543 /** 544 * @return the max height in pixels the description can be such that the 545 * action nicely takes up the entire screen. 546 */ getDescriptionMaxHeight(Context context, TextView title)547 private int getDescriptionMaxHeight(Context context, TextView title) { 548 // The 2 multiplier on the title height calculation is a 549 // conservative estimate for font padding which can not be 550 // calculated at this stage since the view hasn't been rendered yet. 551 return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight()); 552 } 553 554 /** 555 * SelectorAnimator 556 * Controls animation for selected item backgrounds 557 * TODO: Move into focus animation override? 558 */ 559 private static class SelectorAnimator extends RecyclerView.OnScrollListener { 560 561 private final View mSelectorView; 562 private final ViewGroup mParentView; 563 private volatile boolean mFadedOut = true; 564 SelectorAnimator(View selectorView, ViewGroup parentView)565 SelectorAnimator(View selectorView, ViewGroup parentView) { 566 mSelectorView = selectorView; 567 mParentView = parentView; 568 } 569 570 // We want to fade in the selector if we've stopped scrolling on it. If 571 // we're scrolling, we want to ensure to dim the selector if we haven't 572 // already. We dim the last highlighted view so that while a user is 573 // scrolling, nothing is highlighted. 574 @Override onScrollStateChanged(RecyclerView recyclerView, int newState)575 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 576 Animator animator = null; 577 boolean fadingOut = false; 578 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 579 // The selector starts with a height of 0. In order to scale up from 580 // 0, we first need the set the height to 1 and scale from there. 581 View focusedChild = mParentView.getFocusedChild(); 582 if (focusedChild != null) { 583 int selectorHeight = mSelectorView.getHeight(); 584 float scaleY = (float) focusedChild.getHeight() / selectorHeight; 585 AnimatorSet animators = (AnimatorSet)createAnimator(mSelectorView, 586 R.attr.guidedActionsSelectorShowAnimation); 587 if (mFadedOut) { 588 // selector is completely faded out, so we can just scale before fading in. 589 mSelectorView.setScaleY(scaleY); 590 animator = animators.getChildAnimations().get(0); 591 } else { 592 // selector is not faded out, so we must animate the scale as we fade in. 593 ((ObjectAnimator)animators.getChildAnimations().get(1)) 594 .setFloatValues(scaleY); 595 animator = animators; 596 } 597 } 598 } else { 599 animator = createAnimator(mSelectorView, R.attr.guidedActionsSelectorHideAnimation); 600 fadingOut = true; 601 } 602 if (animator != null) { 603 animator.addListener(new Listener(fadingOut)); 604 animator.start(); 605 } 606 } 607 608 /** 609 * Sets {@link BaseScrollAdapterFragment#mFadedOut} 610 * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff 611 * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0 612 * (faded out). If false the view either has an alpha of 1 (visible) or 613 * is in the process of animating. 614 */ 615 private class Listener implements Animator.AnimatorListener { 616 private boolean mFadingOut; 617 private boolean mCanceled; 618 Listener(boolean fadingOut)619 public Listener(boolean fadingOut) { 620 mFadingOut = fadingOut; 621 } 622 623 @Override onAnimationStart(Animator animation)624 public void onAnimationStart(Animator animation) { 625 if (!mFadingOut) { 626 mFadedOut = false; 627 } 628 } 629 630 @Override onAnimationEnd(Animator animation)631 public void onAnimationEnd(Animator animation) { 632 if (!mCanceled && mFadingOut) { 633 mFadedOut = true; 634 } 635 } 636 637 @Override onAnimationCancel(Animator animation)638 public void onAnimationCancel(Animator animation) { 639 mCanceled = true; 640 } 641 642 @Override onAnimationRepeat(Animator animation)643 public void onAnimationRepeat(Animator animation) { 644 } 645 } 646 } 647 648 } 649