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