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