1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.transition; 18 19 import android.animation.Animator; 20 import android.animation.Animator.AnimatorListener; 21 import android.animation.Animator.AnimatorPauseListener; 22 import android.annotation.IntDef; 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.util.AttributeSet; 26 import android.view.View; 27 import android.view.ViewGroup; 28 29 import com.android.internal.R; 30 31 import java.lang.annotation.Retention; 32 import java.lang.annotation.RetentionPolicy; 33 34 /** 35 * This transition tracks changes to the visibility of target views in the 36 * start and end scenes. Visibility is determined not just by the 37 * {@link View#setVisibility(int)} state of views, but also whether 38 * views exist in the current view hierarchy. The class is intended to be a 39 * utility for subclasses such as {@link Fade}, which use this visibility 40 * information to determine the specific animations to run when visibility 41 * changes occur. Subclasses should implement one or both of the methods 42 * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}, 43 * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or 44 * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}, 45 * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}. 46 */ 47 public abstract class Visibility extends Transition { 48 49 static final String PROPNAME_VISIBILITY = "android:visibility:visibility"; 50 private static final String PROPNAME_PARENT = "android:visibility:parent"; 51 private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation"; 52 53 /** @hide */ 54 @Retention(RetentionPolicy.SOURCE) 55 @IntDef(flag = true, prefix = { "MODE_" }, value = { 56 MODE_IN, 57 MODE_OUT 58 }) 59 @interface VisibilityMode {} 60 61 /** 62 * Mode used in {@link #setMode(int)} to make the transition 63 * operate on targets that are appearing. Maybe be combined with 64 * {@link #MODE_OUT} to target Visibility changes both in and out. 65 */ 66 public static final int MODE_IN = 0x1; 67 68 /** 69 * Mode used in {@link #setMode(int)} to make the transition 70 * operate on targets that are disappearing. Maybe be combined with 71 * {@link #MODE_IN} to target Visibility changes both in and out. 72 */ 73 public static final int MODE_OUT = 0x2; 74 75 private static final String[] sTransitionProperties = { 76 PROPNAME_VISIBILITY, 77 PROPNAME_PARENT, 78 }; 79 80 private static class VisibilityInfo { 81 boolean visibilityChange; 82 boolean fadeIn; 83 int startVisibility; 84 int endVisibility; 85 ViewGroup startParent; 86 ViewGroup endParent; 87 } 88 89 private int mMode = MODE_IN | MODE_OUT; 90 private boolean mSuppressLayout = true; 91 Visibility()92 public Visibility() {} 93 Visibility(Context context, AttributeSet attrs)94 public Visibility(Context context, AttributeSet attrs) { 95 super(context, attrs); 96 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VisibilityTransition); 97 int mode = a.getInt(R.styleable.VisibilityTransition_transitionVisibilityMode, 0); 98 a.recycle(); 99 if (mode != 0) { 100 setMode(mode); 101 } 102 } 103 104 /** 105 * This tells the Visibility transition to suppress layout during the transition and release 106 * the suppression after the transition. 107 * @hide 108 */ setSuppressLayout(boolean suppress)109 public void setSuppressLayout(boolean suppress) { 110 this.mSuppressLayout = suppress; 111 } 112 113 /** 114 * Changes the transition to support appearing and/or disappearing Views, depending 115 * on <code>mode</code>. 116 * 117 * @param mode The behavior supported by this transition, a combination of 118 * {@link #MODE_IN} and {@link #MODE_OUT}. 119 * @attr ref android.R.styleable#VisibilityTransition_transitionVisibilityMode 120 */ setMode(@isibilityMode int mode)121 public void setMode(@VisibilityMode int mode) { 122 if ((mode & ~(MODE_IN | MODE_OUT)) != 0) { 123 throw new IllegalArgumentException("Only MODE_IN and MODE_OUT flags are allowed"); 124 } 125 mMode = mode; 126 } 127 128 /** 129 * Returns whether appearing and/or disappearing Views are supported. 130 * 131 * Returns whether appearing and/or disappearing Views are supported. A combination of 132 * {@link #MODE_IN} and {@link #MODE_OUT}. 133 * @attr ref android.R.styleable#VisibilityTransition_transitionVisibilityMode 134 */ 135 @VisibilityMode getMode()136 public int getMode() { 137 return mMode; 138 } 139 140 @Override getTransitionProperties()141 public String[] getTransitionProperties() { 142 return sTransitionProperties; 143 } 144 captureValues(TransitionValues transitionValues)145 private void captureValues(TransitionValues transitionValues) { 146 int visibility = transitionValues.view.getVisibility(); 147 transitionValues.values.put(PROPNAME_VISIBILITY, visibility); 148 transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent()); 149 int[] loc = new int[2]; 150 transitionValues.view.getLocationOnScreen(loc); 151 transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc); 152 } 153 154 @Override captureStartValues(TransitionValues transitionValues)155 public void captureStartValues(TransitionValues transitionValues) { 156 captureValues(transitionValues); 157 } 158 159 @Override captureEndValues(TransitionValues transitionValues)160 public void captureEndValues(TransitionValues transitionValues) { 161 captureValues(transitionValues); 162 } 163 164 /** 165 * Returns whether the view is 'visible' according to the given values 166 * object. This is determined by testing the same properties in the values 167 * object that are used to determine whether the object is appearing or 168 * disappearing in the {@link 169 * Transition#createAnimator(ViewGroup, TransitionValues, TransitionValues)} 170 * method. This method can be called by, for example, subclasses that want 171 * to know whether the object is visible in the same way that Visibility 172 * determines it for the actual animation. 173 * 174 * @param values The TransitionValues object that holds the information by 175 * which visibility is determined. 176 * @return True if the view reference by <code>values</code> is visible, 177 * false otherwise. 178 */ isVisible(TransitionValues values)179 public boolean isVisible(TransitionValues values) { 180 if (values == null) { 181 return false; 182 } 183 int visibility = (Integer) values.values.get(PROPNAME_VISIBILITY); 184 View parent = (View) values.values.get(PROPNAME_PARENT); 185 186 return visibility == View.VISIBLE && parent != null; 187 } 188 getVisibilityChangeInfo(TransitionValues startValues, TransitionValues endValues)189 private static VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues, 190 TransitionValues endValues) { 191 final VisibilityInfo visInfo = new VisibilityInfo(); 192 visInfo.visibilityChange = false; 193 visInfo.fadeIn = false; 194 if (startValues != null && startValues.values.containsKey(PROPNAME_VISIBILITY)) { 195 visInfo.startVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY); 196 visInfo.startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT); 197 } else { 198 visInfo.startVisibility = -1; 199 visInfo.startParent = null; 200 } 201 if (endValues != null && endValues.values.containsKey(PROPNAME_VISIBILITY)) { 202 visInfo.endVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY); 203 visInfo.endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT); 204 } else { 205 visInfo.endVisibility = -1; 206 visInfo.endParent = null; 207 } 208 if (startValues != null && endValues != null) { 209 if (visInfo.startVisibility == visInfo.endVisibility && 210 visInfo.startParent == visInfo.endParent) { 211 return visInfo; 212 } else { 213 if (visInfo.startVisibility != visInfo.endVisibility) { 214 if (visInfo.startVisibility == View.VISIBLE) { 215 visInfo.fadeIn = false; 216 visInfo.visibilityChange = true; 217 } else if (visInfo.endVisibility == View.VISIBLE) { 218 visInfo.fadeIn = true; 219 visInfo.visibilityChange = true; 220 } 221 // no visibilityChange if going between INVISIBLE and GONE 222 } else if (visInfo.startParent != visInfo.endParent) { 223 if (visInfo.endParent == null) { 224 visInfo.fadeIn = false; 225 visInfo.visibilityChange = true; 226 } else if (visInfo.startParent == null) { 227 visInfo.fadeIn = true; 228 visInfo.visibilityChange = true; 229 } 230 } 231 } 232 } else if (startValues == null && visInfo.endVisibility == View.VISIBLE) { 233 visInfo.fadeIn = true; 234 visInfo.visibilityChange = true; 235 } else if (endValues == null && visInfo.startVisibility == View.VISIBLE) { 236 visInfo.fadeIn = false; 237 visInfo.visibilityChange = true; 238 } 239 return visInfo; 240 } 241 242 @Override createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)243 public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, 244 TransitionValues endValues) { 245 VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues); 246 if (visInfo.visibilityChange 247 && (visInfo.startParent != null || visInfo.endParent != null)) { 248 if (visInfo.fadeIn) { 249 return onAppear(sceneRoot, startValues, visInfo.startVisibility, 250 endValues, visInfo.endVisibility); 251 } else { 252 return onDisappear(sceneRoot, startValues, visInfo.startVisibility, 253 endValues, visInfo.endVisibility 254 ); 255 } 256 } 257 return null; 258 } 259 260 /** 261 * The default implementation of this method calls 262 * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}. 263 * Subclasses should override this method or 264 * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}. 265 * if they need to create an Animator when targets appear. 266 * The method should only be called by the Visibility class; it is 267 * not intended to be called from external classes. 268 * 269 * @param sceneRoot The root of the transition hierarchy 270 * @param startValues The target values in the start scene 271 * @param startVisibility The target visibility in the start scene 272 * @param endValues The target values in the end scene 273 * @param endVisibility The target visibility in the end scene 274 * @return An Animator to be started at the appropriate time in the 275 * overall transition for this scene change. A null value means no animation 276 * should be run. 277 */ onAppear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility)278 public Animator onAppear(ViewGroup sceneRoot, 279 TransitionValues startValues, int startVisibility, 280 TransitionValues endValues, int endVisibility) { 281 if ((mMode & MODE_IN) != MODE_IN || endValues == null) { 282 return null; 283 } 284 if (startValues == null) { 285 VisibilityInfo parentVisibilityInfo = null; 286 View endParent = (View) endValues.view.getParent(); 287 TransitionValues startParentValues = getMatchedTransitionValues(endParent, 288 false); 289 TransitionValues endParentValues = getTransitionValues(endParent, false); 290 parentVisibilityInfo = 291 getVisibilityChangeInfo(startParentValues, endParentValues); 292 if (parentVisibilityInfo.visibilityChange) { 293 return null; 294 } 295 } 296 return onAppear(sceneRoot, endValues.view, startValues, endValues); 297 } 298 299 /** 300 * The default implementation of this method returns a null Animator. Subclasses should 301 * override this method to make targets appear with the desired transition. The 302 * method should only be called from 303 * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}. 304 * 305 * @param sceneRoot The root of the transition hierarchy 306 * @param view The View to make appear. This will be in the target scene's View hierarchy and 307 * will be VISIBLE. 308 * @param startValues The target values in the start scene 309 * @param endValues The target values in the end scene 310 * @return An Animator to be started at the appropriate time in the 311 * overall transition for this scene change. A null value means no animation 312 * should be run. 313 */ onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)314 public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, 315 TransitionValues endValues) { 316 return null; 317 } 318 319 /** 320 * Subclasses should override this method or 321 * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)} 322 * if they need to create an Animator when targets disappear. 323 * The method should only be called by the Visibility class; it is 324 * not intended to be called from external classes. 325 * <p> 326 * The default implementation of this method attempts to find a View to use to call 327 * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}, 328 * based on the situation of the View in the View hierarchy. For example, 329 * if a View was simply removed from its parent, then the View will be added 330 * into a {@link android.view.ViewGroupOverlay} and passed as the <code>view</code> 331 * parameter in {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}. 332 * If a visible View is changed to be {@link View#GONE} or {@link View#INVISIBLE}, 333 * then it can be used as the <code>view</code> and the visibility will be changed 334 * to {@link View#VISIBLE} for the duration of the animation. However, if a View 335 * is in a hierarchy which is also altering its visibility, the situation can be 336 * more complicated. In general, if a view that is no longer in the hierarchy in 337 * the end scene still has a parent (so its parent hierarchy was removed, but it 338 * was not removed from its parent), then it will be left alone to avoid side-effects from 339 * improperly removing it from its parent. The only exception to this is if 340 * the previous {@link Scene} was {@link Scene#getSceneForLayout(ViewGroup, int, 341 * android.content.Context) created from a layout resource file}, then it is considered 342 * safe to un-parent the starting scene view in order to make it disappear.</p> 343 * 344 * @param sceneRoot The root of the transition hierarchy 345 * @param startValues The target values in the start scene 346 * @param startVisibility The target visibility in the start scene 347 * @param endValues The target values in the end scene 348 * @param endVisibility The target visibility in the end scene 349 * @return An Animator to be started at the appropriate time in the 350 * overall transition for this scene change. A null value means no animation 351 * should be run. 352 */ onDisappear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility)353 public Animator onDisappear(ViewGroup sceneRoot, 354 TransitionValues startValues, int startVisibility, 355 TransitionValues endValues, int endVisibility) { 356 if ((mMode & MODE_OUT) != MODE_OUT) { 357 return null; 358 } 359 360 View startView = (startValues != null) ? startValues.view : null; 361 View endView = (endValues != null) ? endValues.view : null; 362 View overlayView = null; 363 View viewToKeep = null; 364 if (endView == null || endView.getParent() == null) { 365 if (endView != null) { 366 // endView was removed from its parent - add it to the overlay 367 overlayView = endView; 368 } else if (startView != null) { 369 // endView does not exist. Use startView only under certain 370 // conditions, because placing a view in an overlay necessitates 371 // it being removed from its current parent 372 if (startView.getParent() == null) { 373 // no parent - safe to use 374 overlayView = startView; 375 } else if (startView.getParent() instanceof View) { 376 View startParent = (View) startView.getParent(); 377 TransitionValues startParentValues = getTransitionValues(startParent, true); 378 TransitionValues endParentValues = getMatchedTransitionValues(startParent, 379 true); 380 VisibilityInfo parentVisibilityInfo = 381 getVisibilityChangeInfo(startParentValues, endParentValues); 382 if (!parentVisibilityInfo.visibilityChange) { 383 overlayView = TransitionUtils.copyViewImage(sceneRoot, startView, 384 startParent); 385 } else if (startParent.getParent() == null) { 386 int id = startParent.getId(); 387 if (id != View.NO_ID && sceneRoot.findViewById(id) != null 388 && mCanRemoveViews) { 389 // no parent, but its parent is unparented but the parent 390 // hierarchy has been replaced by a new hierarchy with the same id 391 // and it is safe to un-parent startView 392 overlayView = startView; 393 } 394 } 395 } 396 } 397 } else { 398 // visibility change 399 if (endVisibility == View.INVISIBLE) { 400 viewToKeep = endView; 401 } else { 402 // Becoming GONE 403 if (startView == endView) { 404 viewToKeep = endView; 405 } else if (mCanRemoveViews) { 406 overlayView = startView; 407 } else { 408 overlayView = TransitionUtils.copyViewImage(sceneRoot, startView, 409 (View) startView.getParent()); 410 } 411 } 412 } 413 final int finalVisibility = endVisibility; 414 final ViewGroup finalSceneRoot = sceneRoot; 415 416 if (overlayView != null) { 417 // TODO: Need to do this for general case of adding to overlay 418 int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION); 419 int screenX = screenLoc[0]; 420 int screenY = screenLoc[1]; 421 int[] loc = new int[2]; 422 sceneRoot.getLocationOnScreen(loc); 423 overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft()); 424 overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop()); 425 sceneRoot.getOverlay().add(overlayView); 426 Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues); 427 if (animator == null) { 428 sceneRoot.getOverlay().remove(overlayView); 429 } else { 430 final View finalOverlayView = overlayView; 431 addListener(new TransitionListenerAdapter() { 432 @Override 433 public void onTransitionEnd(Transition transition) { 434 finalSceneRoot.getOverlay().remove(finalOverlayView); 435 transition.removeListener(this); 436 } 437 }); 438 } 439 return animator; 440 } 441 442 if (viewToKeep != null) { 443 int originalVisibility = viewToKeep.getVisibility(); 444 viewToKeep.setTransitionVisibility(View.VISIBLE); 445 Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues); 446 if (animator != null) { 447 DisappearListener disappearListener = new DisappearListener(viewToKeep, 448 finalVisibility, mSuppressLayout); 449 animator.addListener(disappearListener); 450 animator.addPauseListener(disappearListener); 451 addListener(disappearListener); 452 } else { 453 viewToKeep.setTransitionVisibility(originalVisibility); 454 } 455 return animator; 456 } 457 return null; 458 } 459 460 @Override isTransitionRequired(TransitionValues startValues, TransitionValues newValues)461 public boolean isTransitionRequired(TransitionValues startValues, TransitionValues newValues) { 462 if (startValues == null && newValues == null) { 463 return false; 464 } 465 if (startValues != null && newValues != null && 466 newValues.values.containsKey(PROPNAME_VISIBILITY) != 467 startValues.values.containsKey(PROPNAME_VISIBILITY)) { 468 // The transition wasn't targeted in either the start or end, so it couldn't 469 // have changed. 470 return false; 471 } 472 VisibilityInfo changeInfo = getVisibilityChangeInfo(startValues, newValues); 473 return changeInfo.visibilityChange && (changeInfo.startVisibility == View.VISIBLE || 474 changeInfo.endVisibility == View.VISIBLE); 475 } 476 477 /** 478 * The default implementation of this method returns a null Animator. Subclasses should 479 * override this method to make targets disappear with the desired transition. The 480 * method should only be called from 481 * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}. 482 * 483 * @param sceneRoot The root of the transition hierarchy 484 * @param view The View to make disappear. This will be in the target scene's View 485 * hierarchy or in an {@link android.view.ViewGroupOverlay} and will be 486 * VISIBLE. 487 * @param startValues The target values in the start scene 488 * @param endValues The target values in the end scene 489 * @return An Animator to be started at the appropriate time in the 490 * overall transition for this scene change. A null value means no animation 491 * should be run. 492 */ onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)493 public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, 494 TransitionValues endValues) { 495 return null; 496 } 497 498 private static class DisappearListener 499 extends TransitionListenerAdapter implements AnimatorListener, AnimatorPauseListener { 500 private final View mView; 501 private final int mFinalVisibility; 502 private final ViewGroup mParent; 503 private final boolean mSuppressLayout; 504 505 private boolean mLayoutSuppressed; 506 boolean mCanceled = false; 507 DisappearListener(View view, int finalVisibility, boolean suppressLayout)508 public DisappearListener(View view, int finalVisibility, boolean suppressLayout) { 509 this.mView = view; 510 this.mFinalVisibility = finalVisibility; 511 this.mParent = (ViewGroup) view.getParent(); 512 this.mSuppressLayout = suppressLayout; 513 // Prevent a layout from including mView in its calculation. 514 suppressLayout(true); 515 } 516 517 @Override onAnimationPause(Animator animation)518 public void onAnimationPause(Animator animation) { 519 if (!mCanceled) { 520 mView.setTransitionVisibility(mFinalVisibility); 521 } 522 } 523 524 @Override onAnimationResume(Animator animation)525 public void onAnimationResume(Animator animation) { 526 if (!mCanceled) { 527 mView.setTransitionVisibility(View.VISIBLE); 528 } 529 } 530 531 @Override onAnimationCancel(Animator animation)532 public void onAnimationCancel(Animator animation) { 533 mCanceled = true; 534 } 535 536 @Override onAnimationRepeat(Animator animation)537 public void onAnimationRepeat(Animator animation) { 538 } 539 540 @Override onAnimationStart(Animator animation)541 public void onAnimationStart(Animator animation) { 542 } 543 544 @Override onAnimationEnd(Animator animation)545 public void onAnimationEnd(Animator animation) { 546 hideViewWhenNotCanceled(); 547 } 548 549 @Override onTransitionEnd(Transition transition)550 public void onTransitionEnd(Transition transition) { 551 hideViewWhenNotCanceled(); 552 transition.removeListener(this); 553 } 554 555 @Override onTransitionPause(Transition transition)556 public void onTransitionPause(Transition transition) { 557 suppressLayout(false); 558 } 559 560 @Override onTransitionResume(Transition transition)561 public void onTransitionResume(Transition transition) { 562 suppressLayout(true); 563 } 564 hideViewWhenNotCanceled()565 private void hideViewWhenNotCanceled() { 566 if (!mCanceled) { 567 // Recreate the parent's display list in case it includes mView. 568 mView.setTransitionVisibility(mFinalVisibility); 569 if (mParent != null) { 570 mParent.invalidate(); 571 } 572 } 573 // Layout is allowed now that the View is in its final state 574 suppressLayout(false); 575 } 576 suppressLayout(boolean suppress)577 private void suppressLayout(boolean suppress) { 578 if (mSuppressLayout && mLayoutSuppressed != suppress && mParent != null) { 579 mLayoutSuppressed = suppress; 580 mParent.suppressLayout(suppress); 581 } 582 } 583 } 584 } 585