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.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.animation.PropertyValuesHolder; 23 import android.animation.RectEvaluator; 24 import android.graphics.Bitmap; 25 import android.graphics.Canvas; 26 import android.graphics.Rect; 27 import android.graphics.drawable.BitmapDrawable; 28 import android.view.View; 29 import android.view.ViewGroup; 30 31 import java.util.Map; 32 33 /** 34 * This transition captures the layout bounds of target views before and after 35 * the scene change and animates those changes during the transition. 36 * 37 * <p>A ChangeBounds transition can be described in a resource file by using the 38 * tag <code>changeBounds</code>, along with the other standard 39 * attributes of {@link android.R.styleable#Transition}.</p> 40 */ 41 public class ChangeBounds extends Transition { 42 43 private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds"; 44 private static final String PROPNAME_PARENT = "android:changeBounds:parent"; 45 private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX"; 46 private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY"; 47 private static final String[] sTransitionProperties = { 48 PROPNAME_BOUNDS, 49 PROPNAME_PARENT, 50 PROPNAME_WINDOW_X, 51 PROPNAME_WINDOW_Y 52 }; 53 54 int[] tempLocation = new int[2]; 55 boolean mResizeClip = false; 56 boolean mReparent = false; 57 private static final String LOG_TAG = "ChangeBounds"; 58 59 private static RectEvaluator sRectEvaluator = new RectEvaluator(); 60 61 @Override getTransitionProperties()62 public String[] getTransitionProperties() { 63 return sTransitionProperties; 64 } 65 setResizeClip(boolean resizeClip)66 public void setResizeClip(boolean resizeClip) { 67 mResizeClip = resizeClip; 68 } 69 70 /** 71 * Setting this flag tells ChangeBounds to track the before/after parent 72 * of every view using this transition. The flag is not enabled by 73 * default because it requires the parent instances to be the same 74 * in the two scenes or else all parents must use ids to allow 75 * the transition to determine which parents are the same. 76 * 77 * @param reparent true if the transition should track the parent 78 * container of target views and animate parent changes. 79 */ setReparent(boolean reparent)80 public void setReparent(boolean reparent) { 81 mReparent = reparent; 82 } 83 captureValues(TransitionValues values)84 private void captureValues(TransitionValues values) { 85 View view = values.view; 86 values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(), 87 view.getRight(), view.getBottom())); 88 values.values.put(PROPNAME_PARENT, values.view.getParent()); 89 values.view.getLocationInWindow(tempLocation); 90 values.values.put(PROPNAME_WINDOW_X, tempLocation[0]); 91 values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]); 92 } 93 94 @Override captureStartValues(TransitionValues transitionValues)95 public void captureStartValues(TransitionValues transitionValues) { 96 captureValues(transitionValues); 97 } 98 99 @Override captureEndValues(TransitionValues transitionValues)100 public void captureEndValues(TransitionValues transitionValues) { 101 captureValues(transitionValues); 102 } 103 104 @Override createAnimator(final ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)105 public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues, 106 TransitionValues endValues) { 107 if (startValues == null || endValues == null) { 108 return null; 109 } 110 Map<String, Object> startParentVals = startValues.values; 111 Map<String, Object> endParentVals = endValues.values; 112 ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT); 113 ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT); 114 if (startParent == null || endParent == null) { 115 return null; 116 } 117 final View view = endValues.view; 118 boolean parentsEqual = (startParent == endParent) || 119 (startParent.getId() == endParent.getId()); 120 // TODO: Might want reparenting to be separate/subclass transition, or at least 121 // triggered by a property on ChangeBounds. Otherwise, we're forcing the requirement that 122 // all parents in layouts have IDs to avoid layout-inflation resulting in a side-effect 123 // of reparenting the views. 124 if (!mReparent || parentsEqual) { 125 Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS); 126 Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); 127 int startLeft = startBounds.left; 128 int endLeft = endBounds.left; 129 int startTop = startBounds.top; 130 int endTop = endBounds.top; 131 int startRight = startBounds.right; 132 int endRight = endBounds.right; 133 int startBottom = startBounds.bottom; 134 int endBottom = endBounds.bottom; 135 int startWidth = startRight - startLeft; 136 int startHeight = startBottom - startTop; 137 int endWidth = endRight - endLeft; 138 int endHeight = endBottom - endTop; 139 int numChanges = 0; 140 if (startWidth != 0 && startHeight != 0 && endWidth != 0 && endHeight != 0) { 141 if (startLeft != endLeft) ++numChanges; 142 if (startTop != endTop) ++numChanges; 143 if (startRight != endRight) ++numChanges; 144 if (startBottom != endBottom) ++numChanges; 145 } 146 if (numChanges > 0) { 147 if (!mResizeClip) { 148 PropertyValuesHolder pvh[] = new PropertyValuesHolder[numChanges]; 149 int pvhIndex = 0; 150 if (startLeft != endLeft) view.setLeft(startLeft); 151 if (startTop != endTop) view.setTop(startTop); 152 if (startRight != endRight) view.setRight(startRight); 153 if (startBottom != endBottom) view.setBottom(startBottom); 154 if (startLeft != endLeft) { 155 pvh[pvhIndex++] = PropertyValuesHolder.ofInt("left", startLeft, endLeft); 156 } 157 if (startTop != endTop) { 158 pvh[pvhIndex++] = PropertyValuesHolder.ofInt("top", startTop, endTop); 159 } 160 if (startRight != endRight) { 161 pvh[pvhIndex++] = PropertyValuesHolder.ofInt("right", 162 startRight, endRight); 163 } 164 if (startBottom != endBottom) { 165 pvh[pvhIndex++] = PropertyValuesHolder.ofInt("bottom", 166 startBottom, endBottom); 167 } 168 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view, pvh); 169 if (view.getParent() instanceof ViewGroup) { 170 final ViewGroup parent = (ViewGroup) view.getParent(); 171 parent.suppressLayout(true); 172 TransitionListener transitionListener = new TransitionListenerAdapter() { 173 boolean mCanceled = false; 174 175 @Override 176 public void onTransitionCancel(Transition transition) { 177 parent.suppressLayout(false); 178 mCanceled = true; 179 } 180 181 @Override 182 public void onTransitionEnd(Transition transition) { 183 if (!mCanceled) { 184 parent.suppressLayout(false); 185 } 186 } 187 188 @Override 189 public void onTransitionPause(Transition transition) { 190 parent.suppressLayout(false); 191 } 192 193 @Override 194 public void onTransitionResume(Transition transition) { 195 parent.suppressLayout(true); 196 } 197 }; 198 addListener(transitionListener); 199 } 200 return anim; 201 } else { 202 if (startWidth != endWidth) view.setRight(endLeft + 203 Math.max(startWidth, endWidth)); 204 if (startHeight != endHeight) view.setBottom(endTop + 205 Math.max(startHeight, endHeight)); 206 // TODO: don't clobber TX/TY 207 if (startLeft != endLeft) view.setTranslationX(startLeft - endLeft); 208 if (startTop != endTop) view.setTranslationY(startTop - endTop); 209 // Animate location with translationX/Y and size with clip bounds 210 float transXDelta = endLeft - startLeft; 211 float transYDelta = endTop - startTop; 212 int widthDelta = endWidth - startWidth; 213 int heightDelta = endHeight - startHeight; 214 numChanges = 0; 215 if (transXDelta != 0) numChanges++; 216 if (transYDelta != 0) numChanges++; 217 if (widthDelta != 0 || heightDelta != 0) numChanges++; 218 PropertyValuesHolder pvh[] = new PropertyValuesHolder[numChanges]; 219 int pvhIndex = 0; 220 if (transXDelta != 0) { 221 pvh[pvhIndex++] = PropertyValuesHolder.ofFloat("translationX", 222 view.getTranslationX(), 0); 223 } 224 if (transYDelta != 0) { 225 pvh[pvhIndex++] = PropertyValuesHolder.ofFloat("translationY", 226 view.getTranslationY(), 0); 227 } 228 if (widthDelta != 0 || heightDelta != 0) { 229 Rect tempStartBounds = new Rect(0, 0, startWidth, startHeight); 230 Rect tempEndBounds = new Rect(0, 0, endWidth, endHeight); 231 pvh[pvhIndex++] = PropertyValuesHolder.ofObject("clipBounds", 232 sRectEvaluator, tempStartBounds, tempEndBounds); 233 } 234 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view, pvh); 235 if (view.getParent() instanceof ViewGroup) { 236 final ViewGroup parent = (ViewGroup) view.getParent(); 237 parent.suppressLayout(true); 238 TransitionListener transitionListener = new TransitionListenerAdapter() { 239 boolean mCanceled = false; 240 241 @Override 242 public void onTransitionCancel(Transition transition) { 243 parent.suppressLayout(false); 244 mCanceled = true; 245 } 246 247 @Override 248 public void onTransitionEnd(Transition transition) { 249 if (!mCanceled) { 250 parent.suppressLayout(false); 251 } 252 } 253 254 @Override 255 public void onTransitionPause(Transition transition) { 256 parent.suppressLayout(false); 257 } 258 259 @Override 260 public void onTransitionResume(Transition transition) { 261 parent.suppressLayout(true); 262 } 263 }; 264 addListener(transitionListener); 265 } 266 anim.addListener(new AnimatorListenerAdapter() { 267 @Override 268 public void onAnimationEnd(Animator animation) { 269 view.setClipBounds(null); 270 } 271 }); 272 return anim; 273 } 274 } 275 } else { 276 int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X); 277 int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y); 278 int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X); 279 int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y); 280 // TODO: also handle size changes: check bounds and animate size changes 281 if (startX != endX || startY != endY) { 282 sceneRoot.getLocationInWindow(tempLocation); 283 Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), 284 Bitmap.Config.ARGB_8888); 285 Canvas canvas = new Canvas(bitmap); 286 view.draw(canvas); 287 final BitmapDrawable drawable = new BitmapDrawable(bitmap); 288 view.setVisibility(View.INVISIBLE); 289 sceneRoot.getOverlay().add(drawable); 290 Rect startBounds1 = new Rect(startX - tempLocation[0], startY - tempLocation[1], 291 startX - tempLocation[0] + view.getWidth(), 292 startY - tempLocation[1] + view.getHeight()); 293 Rect endBounds1 = new Rect(endX - tempLocation[0], endY - tempLocation[1], 294 endX - tempLocation[0] + view.getWidth(), 295 endY - tempLocation[1] + view.getHeight()); 296 ObjectAnimator anim = ObjectAnimator.ofObject(drawable, "bounds", 297 sRectEvaluator, startBounds1, endBounds1); 298 anim.addListener(new AnimatorListenerAdapter() { 299 @Override 300 public void onAnimationEnd(Animator animation) { 301 sceneRoot.getOverlay().remove(drawable); 302 view.setVisibility(View.VISIBLE); 303 } 304 }); 305 return anim; 306 } 307 } 308 return null; 309 } 310 } 311