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