GuidedActionsStylist.java revision e2f7aef2f45dcdfe116995b64f9a7be5c68a36a1
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 * 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 protected View mMainView; 246 protected VerticalGridView mActionsGridView; 247 protected View mSelectorView; 248 249 // Cached values from resources 250 private float mEnabledTextAlpha; 251 private float mDisabledTextAlpha; 252 private float mEnabledDescriptionAlpha; 253 private float mDisabledDescriptionAlpha; 254 private float mEnabledChevronAlpha; 255 private float mDisabledChevronAlpha; 256 private int mContentWidth; 257 private int mContentWidthNoIcon; 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 if (mMainView instanceof VerticalGridView) { 279 mActionsGridView = (VerticalGridView) mMainView; 280 } else { 281 mActionsGridView = (VerticalGridView) mMainView.findViewById(R.id.guidedactions_list); 282 if (mActionsGridView == null) { 283 throw new IllegalStateException("No ListView exists."); 284 } 285 mActionsGridView.setWindowAlignmentOffset(0); 286 mActionsGridView.setWindowAlignmentOffsetPercent(50f); 287 mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 288 if (mSelectorView != null) { 289 mActionsGridView.setOnScrollListener(new 290 SelectorAnimator(mSelectorView, mActionsGridView)); 291 } 292 } 293 294 mActionsGridView.requestFocusFromTouch(); 295 296 if (mSelectorView != null) { 297 // ALlow focus to move to other views 298 mActionsGridView.getViewTreeObserver().addOnGlobalFocusChangeListener( 299 new ViewTreeObserver.OnGlobalFocusChangeListener() { 300 private boolean mChildFocused; 301 302 @Override 303 public void onGlobalFocusChanged(View oldFocus, View newFocus) { 304 View focusedChild = mActionsGridView.getFocusedChild(); 305 if (focusedChild == null) { 306 mSelectorView.setVisibility(View.INVISIBLE); 307 mChildFocused = false; 308 } else if (!mChildFocused) { 309 mChildFocused = true; 310 mSelectorView.setVisibility(View.VISIBLE); 311 updateSelectorView(focusedChild); 312 } 313 } 314 }); 315 } 316 317 // Cache widths, chevron alpha values, max and min text lines, etc 318 Context ctx = mMainView.getContext(); 319 TypedValue val = new TypedValue(); 320 mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha); 321 mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha); 322 mContentWidth = getDimension(ctx, val, R.attr.guidedActionContentWidth); 323 mContentWidthNoIcon = getDimension(ctx, val, R.attr.guidedActionContentWidthNoIcon); 324 mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines); 325 mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines); 326 mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines); 327 mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding); 328 mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)) 329 .getDefaultDisplay().getHeight(); 330 331 mEnabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string 332 .lb_guidedactions_item_unselected_text_alpha)); 333 mDisabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string 334 .lb_guidedactions_item_disabled_text_alpha)); 335 mEnabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string 336 .lb_guidedactions_item_unselected_description_text_alpha)); 337 mDisabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string 338 .lb_guidedactions_item_disabled_description_text_alpha)); 339 return mMainView; 340 } 341 342 /** 343 * Returns the VerticalGridView that displays the list of GuidedActions. 344 * @return The VerticalGridView for this presenter. 345 */ 346 public VerticalGridView getActionsGridView() { 347 return mActionsGridView; 348 } 349 350 /** 351 * Provides the resource ID of the layout defining the host view for the list of guided actions. 352 * Subclasses may override to provide their own customized layouts. The base implementation 353 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions}. If overridden, the 354 * substituted layout should contain matching IDs for any views that should be managed by the 355 * base class; this can be achieved by starting with a copy of the base layout file. 356 * @return The resource ID of the layout to be inflated to define the host view for the list 357 * of GuidedActions. 358 */ 359 public int onProvideLayoutId() { 360 return R.layout.lb_guidedactions; 361 } 362 363 /** 364 * Return view type of action, each different type can have differently associated layout Id. 365 * Default implementation returns {@link #VIEW_TYPE_DEFAULT}. 366 * @param action The action object. 367 * @return View type that used in {@link #onProvideItemLayoutId(int)}. 368 */ 369 public int getItemViewType(GuidedAction action) { 370 return VIEW_TYPE_DEFAULT; 371 } 372 373 /** 374 * Provides the resource ID of the layout defining the view for an individual guided actions. 375 * Subclasses may override to provide their own customized layouts. The base implementation 376 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden, 377 * the substituted layout should contain matching IDs for any views that should be managed by 378 * the base class; this can be achieved by starting with a copy of the base layout file. Note 379 * that in order for the item to support editing, the title view should both subclass {@link 380 * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link 381 * GuidedActionEditText}. To support different types of Layouts, override {@link 382 * #onProvideItemLayoutId(int)}. 383 * @return The resource ID of the layout to be inflated to define the view to display an 384 * individual GuidedAction. 385 */ 386 public int onProvideItemLayoutId() { 387 return R.layout.lb_guidedactions_item; 388 } 389 390 /** 391 * Provides the resource ID of the layout defining the view for an individual guided actions. 392 * Subclasses may override to provide their own customized layouts. The base implementation 393 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden, 394 * the substituted layout should contain matching IDs for any views that should be managed by 395 * the base class; this can be achieved by starting with a copy of the base layout file. Note 396 * that in order for the item to support editing, the title view should both subclass {@link 397 * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link 398 * GuidedActionEditText}. 399 * @param viewType View type returned by {@link #getItemViewType(GuidedAction)} 400 * @return The resource ID of the layout to be inflated to define the view to display an 401 * individual GuidedAction. 402 */ 403 public int onProvideItemLayoutId(int viewType) { 404 if (viewType == VIEW_TYPE_DEFAULT) { 405 return onProvideItemLayoutId(); 406 } else { 407 throw new RuntimeException("ViewType " + viewType + 408 " not supported in GuidedActionsStylist"); 409 } 410 } 411 412 /** 413 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 414 * may choose to return a subclass of ViewHolder. To support different view types, override 415 * {@link #onCreateViewHolder(ViewGroup, int)} 416 * <p> 417 * <i>Note: Should not actually add the created view to the parent; the caller will do 418 * this.</i> 419 * @param parent The view group to be used as the parent of the new view. 420 * @return The view to be added to the caller's view hierarchy. 421 */ 422 public ViewHolder onCreateViewHolder(ViewGroup parent) { 423 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 424 View v = inflater.inflate(onProvideItemLayoutId(), parent, false); 425 return new ViewHolder(v); 426 } 427 428 /** 429 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 430 * may choose to return a subclass of ViewHolder. 431 * <p> 432 * <i>Note: Should not actually add the created view to the parent; the caller will do 433 * this.</i> 434 * @param parent The view group to be used as the parent of the new view. 435 * @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)} 436 * @return The view to be added to the caller's view hierarchy. 437 */ 438 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 439 if (viewType == VIEW_TYPE_DEFAULT) { 440 return onCreateViewHolder(parent); 441 } 442 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 443 View v = inflater.inflate(onProvideItemLayoutId(viewType), parent, false); 444 return new ViewHolder(v); 445 } 446 447 /** 448 * Binds a {@link ViewHolder} to a particular {@link GuidedAction}. 449 * @param vh The view holder to be associated with the given action. 450 * @param action The guided action to be displayed by the view holder's view. 451 * @return The view to be added to the caller's view hierarchy. 452 */ 453 public void onBindViewHolder(ViewHolder vh, GuidedAction action) { 454 455 if (vh.mTitleView != null) { 456 vh.mTitleView.setText(action.getTitle()); 457 vh.mTitleView.setAlpha(action.isEnabled() ? mEnabledTextAlpha : mDisabledTextAlpha); 458 } 459 if (vh.mDescriptionView != null) { 460 vh.mDescriptionView.setText(action.getDescription()); 461 vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ? 462 View.GONE : View.VISIBLE); 463 vh.mDescriptionView.setAlpha(action.isEnabled() ? mEnabledDescriptionAlpha : 464 mDisabledDescriptionAlpha); 465 } 466 // Clients might want the check mark view to be gone entirely, in which case, ignore it. 467 if (vh.mCheckmarkView != null && vh.mCheckmarkView.getVisibility() != View.GONE) { 468 vh.mCheckmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE); 469 } 470 471 if (vh.mContentView != null) { 472 ViewGroup.LayoutParams contentLp = vh.mContentView.getLayoutParams(); 473 if (setIcon(vh.mIconView, action)) { 474 contentLp.width = mContentWidth; 475 } else { 476 contentLp.width = mContentWidthNoIcon; 477 } 478 vh.mContentView.setLayoutParams(contentLp); 479 } 480 481 if (vh.mChevronView != null) { 482 vh.mChevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE); 483 vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha : 484 mDisabledChevronAlpha); 485 } 486 487 if (action.hasMultilineDescription()) { 488 if (vh.mTitleView != null) { 489 vh.mTitleView.setMaxLines(mTitleMaxLines); 490 if (vh.mDescriptionView != null) { 491 vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(vh.view.getContext(), 492 vh.mTitleView)); 493 } 494 } 495 } else { 496 if (vh.mTitleView != null) { 497 vh.mTitleView.setMaxLines(mTitleMinLines); 498 } 499 if (vh.mDescriptionView != null) { 500 vh.mDescriptionView.setMaxLines(mDescriptionMinLines); 501 } 502 } 503 setEditingMode(vh, action, false); 504 if (action.isFocusable()) { 505 vh.view.setFocusable(true); 506 if (vh.view instanceof ViewGroup) { 507 ((ViewGroup) vh.view).setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 508 } 509 } else { 510 vh.view.setFocusable(false); 511 if (vh.view instanceof ViewGroup) { 512 ((ViewGroup) vh.view).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 513 } 514 } 515 } 516 517 public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) { 518 if (editing != vh.mInEditing) { 519 vh.mInEditing = editing; 520 onEditingModeChange(vh, action, editing); 521 } 522 } 523 524 protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) { 525 TextView titleView = vh.getTitleView(); 526 TextView descriptionView = vh.getDescriptionView(); 527 if (editing) { 528 CharSequence editTitle = action.getEditTitle(); 529 if (titleView != null && editTitle != null) { 530 titleView.setText(editTitle); 531 } 532 CharSequence editDescription = action.getEditDescription(); 533 if (descriptionView != null && editDescription != null) { 534 descriptionView.setText(editDescription); 535 } 536 if (action.isDescriptionEditable()) { 537 if (descriptionView != null) { 538 descriptionView.setVisibility(View.VISIBLE); 539 descriptionView.setInputType(action.getDescriptionEditInputType()); 540 } 541 vh.mInEditingDescription = true; 542 } else { 543 vh.mInEditingDescription = false; 544 if (titleView != null) { 545 titleView.setInputType(action.getEditInputType()); 546 } 547 } 548 } else { 549 if (titleView != null) { 550 titleView.setText(action.getTitle()); 551 } 552 if (descriptionView != null) { 553 descriptionView.setText(action.getDescription()); 554 } 555 if (vh.mInEditingDescription) { 556 if (descriptionView != null) { 557 descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ? 558 View.GONE : View.VISIBLE); 559 descriptionView.setInputType(action.getDescriptionInputType()); 560 } 561 vh.mInEditingDescription = false; 562 } else { 563 if (titleView != null) { 564 titleView.setInputType(action.getInputType()); 565 } 566 } 567 } 568 } 569 570 /** 571 * Animates the view holder's view (or subviews thereof) when the action has had its focus 572 * state changed. 573 * @param vh The view holder associated with the relevant action. 574 * @param focused True if the action has become focused, false if it has lost focus. 575 */ 576 public void onAnimateItemFocused(ViewHolder vh, boolean focused) { 577 // No animations for this, currently, because the animation is done on 578 // mSelectorView 579 } 580 581 /** 582 * Animates the view holder's view (or subviews thereof) when the action has had its press 583 * state changed. 584 * @param vh The view holder associated with the relevant action. 585 * @param pressed True if the action has been pressed, false if it has been unpressed. 586 */ 587 public void onAnimateItemPressed(ViewHolder vh, boolean pressed) { 588 int attr = pressed ? R.attr.guidedActionPressedAnimation : 589 R.attr.guidedActionUnpressedAnimation; 590 createAnimator(vh.view, attr).start(); 591 } 592 593 /** 594 * Animates the view holder's view (or subviews thereof) when the action has had its check 595 * state changed. 596 * @param vh The view holder associated with the relevant action. 597 * @param checked True if the action has become checked, false if it has become unchecked. 598 */ 599 public void onAnimateItemChecked(ViewHolder vh, boolean checked) { 600 final View checkView = vh.mCheckmarkView; 601 if (checkView != null) { 602 if (checked) { 603 checkView.setVisibility(View.VISIBLE); 604 createAnimator(checkView, R.attr.guidedActionCheckedAnimation).start(); 605 } else { 606 Animator animator = createAnimator(checkView, 607 R.attr.guidedActionUncheckedAnimation); 608 animator.addListener(new AnimatorListenerAdapter() { 609 @Override 610 public void onAnimationEnd(Animator animation) { 611 checkView.setVisibility(View.INVISIBLE); 612 } 613 }); 614 animator.start(); 615 } 616 } 617 } 618 619 /* 620 * ========================================== 621 * FragmentAnimationProvider overrides 622 * ========================================== 623 */ 624 625 /** 626 * {@inheritDoc} 627 */ 628 @Override 629 public void onImeAppearing(@NonNull List<Animator> animators) { 630 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepImeAppearingAnimation)); 631 animators.add(createAnimator(mSelectorView, R.attr.guidedStepImeAppearingAnimation)); 632 } 633 634 /** 635 * {@inheritDoc} 636 */ 637 @Override 638 public void onImeDisappearing(@NonNull List<Animator> animators) { 639 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepImeDisappearingAnimation)); 640 animators.add(createAnimator(mSelectorView, R.attr.guidedStepImeDisappearingAnimation)); 641 } 642 643 /* 644 * ========================================== 645 * Private methods 646 * ========================================== 647 */ 648 649 private void updateSelectorView(View focusedChild) { 650 // Display the selector view. 651 int height = focusedChild.getHeight(); 652 LayoutParams lp = mSelectorView.getLayoutParams(); 653 lp.height = height; 654 mSelectorView.setLayoutParams(lp); 655 mSelectorView.setAlpha(1f); 656 } 657 658 private float getFloat(Context ctx, TypedValue typedValue, int attrId) { 659 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 660 // Android resources don't have a native float type, so we have to use strings. 661 return Float.valueOf(ctx.getResources().getString(typedValue.resourceId)); 662 } 663 664 private int getInteger(Context ctx, TypedValue typedValue, int attrId) { 665 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 666 return ctx.getResources().getInteger(typedValue.resourceId); 667 } 668 669 private int getDimension(Context ctx, TypedValue typedValue, int attrId) { 670 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 671 return ctx.getResources().getDimensionPixelSize(typedValue.resourceId); 672 } 673 674 private static Animator createAnimator(View v, int attrId) { 675 Context ctx = v.getContext(); 676 TypedValue typedValue = new TypedValue(); 677 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 678 Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId); 679 animator.setTarget(v); 680 return animator; 681 } 682 683 private boolean setIcon(final ImageView iconView, GuidedAction action) { 684 Drawable icon = null; 685 if (iconView != null) { 686 Context context = iconView.getContext(); 687 icon = action.getIcon(); 688 if (icon != null) { 689 // setImageDrawable resets the drawable's level unless we set the view level first. 690 iconView.setImageLevel(icon.getLevel()); 691 iconView.setImageDrawable(icon); 692 iconView.setVisibility(View.VISIBLE); 693 } else { 694 iconView.setVisibility(View.GONE); 695 } 696 } 697 return icon != null; 698 } 699 700 /** 701 * @return the max height in pixels the description can be such that the 702 * action nicely takes up the entire screen. 703 */ 704 private int getDescriptionMaxHeight(Context context, TextView title) { 705 // The 2 multiplier on the title height calculation is a 706 // conservative estimate for font padding which can not be 707 // calculated at this stage since the view hasn't been rendered yet. 708 return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight()); 709 } 710 711 /** 712 * SelectorAnimator 713 * Controls animation for selected item backgrounds 714 * TODO: Move into focus animation override? 715 */ 716 private static class SelectorAnimator extends RecyclerView.OnScrollListener { 717 718 private final View mSelectorView; 719 private final ViewGroup mParentView; 720 private volatile boolean mFadedOut = true; 721 722 SelectorAnimator(View selectorView, ViewGroup parentView) { 723 mSelectorView = selectorView; 724 mParentView = parentView; 725 } 726 727 // We want to fade in the selector if we've stopped scrolling on it. If 728 // we're scrolling, we want to ensure to dim the selector if we haven't 729 // already. We dim the last highlighted view so that while a user is 730 // scrolling, nothing is highlighted. 731 @Override 732 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 733 Animator animator = null; 734 boolean fadingOut = false; 735 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 736 // The selector starts with a height of 0. In order to scale up from 737 // 0, we first need the set the height to 1 and scale from there. 738 View focusedChild = mParentView.getFocusedChild(); 739 if (focusedChild != null) { 740 int selectorHeight = mSelectorView.getHeight(); 741 float scaleY = (float) focusedChild.getHeight() / selectorHeight; 742 AnimatorSet animators = (AnimatorSet)createAnimator(mSelectorView, 743 R.attr.guidedActionsSelectorShowAnimation); 744 if (mFadedOut) { 745 // selector is completely faded out, so we can just scale before fading in. 746 mSelectorView.setScaleY(scaleY); 747 animator = animators.getChildAnimations().get(0); 748 } else { 749 // selector is not faded out, so we must animate the scale as we fade in. 750 ((ObjectAnimator)animators.getChildAnimations().get(1)) 751 .setFloatValues(scaleY); 752 animator = animators; 753 } 754 } 755 } else { 756 animator = createAnimator(mSelectorView, R.attr.guidedActionsSelectorHideAnimation); 757 fadingOut = true; 758 } 759 if (animator != null) { 760 animator.addListener(new Listener(fadingOut)); 761 animator.start(); 762 } 763 } 764 765 /** 766 * Sets {@link BaseScrollAdapterFragment#mFadedOut} 767 * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff 768 * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0 769 * (faded out). If false the view either has an alpha of 1 (visible) or 770 * is in the process of animating. 771 */ 772 private class Listener implements Animator.AnimatorListener { 773 private boolean mFadingOut; 774 private boolean mCanceled; 775 776 public Listener(boolean fadingOut) { 777 mFadingOut = fadingOut; 778 } 779 780 @Override 781 public void onAnimationStart(Animator animation) { 782 if (!mFadingOut) { 783 mFadedOut = false; 784 } 785 } 786 787 @Override 788 public void onAnimationEnd(Animator animation) { 789 if (!mCanceled && mFadingOut) { 790 mFadedOut = true; 791 } 792 } 793 794 @Override 795 public void onAnimationCancel(Animator animation) { 796 mCanceled = true; 797 } 798 799 @Override 800 public void onAnimationRepeat(Animator animation) { 801 } 802 } 803 } 804 805} 806