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