1/* 2 * Copyright (C) 2016 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.support.transition; 18 19import android.R; 20import android.content.Context; 21import android.graphics.Canvas; 22import android.graphics.Rect; 23import android.graphics.drawable.Drawable; 24import android.support.annotation.RestrictTo; 25import android.support.v4.view.ViewCompat; 26import android.view.MotionEvent; 27import android.view.View; 28import android.view.ViewGroup; 29import android.view.ViewParent; 30 31import java.lang.reflect.InvocationTargetException; 32import java.lang.reflect.Method; 33import java.util.ArrayList; 34 35import static android.support.annotation.RestrictTo.Scope.GROUP_ID; 36 37class ViewOverlay { 38 39 /** 40 * The actual container for the drawables (and views, if it's a ViewGroupOverlay). 41 * All of the management and rendering details for the overlay are handled in 42 * OverlayViewGroup. 43 */ 44 protected OverlayViewGroup mOverlayViewGroup; 45 46 ViewOverlay(Context context, ViewGroup hostView, View requestingView) { 47 mOverlayViewGroup = new OverlayViewGroup(context, hostView, requestingView, this); 48 } 49 50 static ViewGroup getContentView(View view) { 51 View parent = view; 52 while (parent != null) { 53 if (parent.getId() == R.id.content && parent instanceof ViewGroup) { 54 return (ViewGroup) parent; 55 } 56 if (parent.getParent() instanceof ViewGroup) { 57 parent = (ViewGroup) parent.getParent(); 58 } 59 } 60 return null; 61 } 62 63 public static ViewOverlay createFrom(View view) { 64 ViewGroup contentView = getContentView(view); 65 if (contentView != null) { 66 final int numChildren = contentView.getChildCount(); 67 for (int i = 0; i < numChildren; ++i) { 68 View child = contentView.getChildAt(i); 69 if (child instanceof OverlayViewGroup) { 70 return ((OverlayViewGroup) child).mViewOverlay; 71 } 72 } 73 return new ViewGroupOverlay(contentView.getContext(), contentView, view); 74 } 75 return null; 76 } 77 78 /** 79 * Used internally by View and ViewGroup to handle drawing and invalidation 80 * of the overlay 81 */ 82 ViewGroup getOverlayView() { 83 return mOverlayViewGroup; 84 } 85 86 /** 87 * Adds a Drawable to the overlay. The bounds of the drawable should be relative to 88 * the host view. Any drawable added to the overlay should be removed when it is no longer 89 * needed or no longer visible. 90 * 91 * @param drawable The Drawable to be added to the overlay. This drawable will be 92 * drawn when the view redraws its overlay. 93 * @see #remove(Drawable) 94 */ 95 public void add(Drawable drawable) { 96 mOverlayViewGroup.add(drawable); 97 } 98 99 /** 100 * Removes the specified Drawable from the overlay. 101 * 102 * @param drawable The Drawable to be removed from the overlay. 103 * @see #add(Drawable) 104 */ 105 public void remove(Drawable drawable) { 106 mOverlayViewGroup.remove(drawable); 107 } 108 109 /** 110 * Removes all content from the overlay. 111 */ 112 public void clear() { 113 mOverlayViewGroup.clear(); 114 } 115 116 boolean isEmpty() { 117 return mOverlayViewGroup.isEmpty(); 118 } 119 120 /** 121 * OverlayViewGroup is a container that View and ViewGroup use to host 122 * drawables and views added to their overlays ({@link ViewOverlay} and 123 * {@link ViewGroupOverlay}, respectively). Drawables are added to the overlay 124 * via the add/remove methods in ViewOverlay, Views are added/removed via 125 * ViewGroupOverlay. These drawable and view objects are 126 * drawn whenever the view itself is drawn; first the view draws its own 127 * content (and children, if it is a ViewGroup), then it draws its overlay 128 * (if it has one). 129 * 130 * <p>Besides managing and drawing the list of drawables, this class serves 131 * two purposes: 132 * (1) it noops layout calls because children are absolutely positioned and 133 * (2) it forwards all invalidation calls to its host view. The invalidation 134 * redirect is necessary because the overlay is not a child of the host view 135 * and invalidation cannot therefore follow the normal path up through the 136 * parent hierarchy.</p> 137 * 138 * @see View#getOverlay() 139 * @see ViewGroup#getOverlay() 140 */ 141 static class OverlayViewGroup extends ViewGroup { 142 143 static Method sInvalidateChildInParentFastMethod; 144 145 static { 146 try { 147 sInvalidateChildInParentFastMethod = ViewGroup.class.getDeclaredMethod( 148 "invalidateChildInParentFast", int.class, int.class, Rect.class); 149 } catch (NoSuchMethodException e) { 150 } 151 152 } 153 154 /** 155 * The View for which this is an overlay. Invalidations of the overlay are redirected to 156 * this host view. 157 */ 158 ViewGroup mHostView; 159 View mRequestingView; 160 /** 161 * The set of drawables to draw when the overlay is rendered. 162 */ 163 ArrayList<Drawable> mDrawables = null; 164 /** 165 * Reference to the hosting overlay object 166 */ 167 ViewOverlay mViewOverlay; 168 169 OverlayViewGroup(Context context, ViewGroup hostView, View requestingView, 170 ViewOverlay viewOverlay) { 171 super(context); 172 mHostView = hostView; 173 mRequestingView = requestingView; 174 setRight(hostView.getWidth()); 175 setBottom(hostView.getHeight()); 176 ((ViewGroup) hostView).addView(this); 177 mViewOverlay = viewOverlay; 178 } 179 180 @Override 181 public boolean dispatchTouchEvent(MotionEvent ev) { 182 // Intercept and noop all touch events - overlays do not allow touch events 183 return false; 184 } 185 186 public void add(Drawable drawable) { 187 if (mDrawables == null) { 188 189 mDrawables = new ArrayList<Drawable>(); 190 } 191 if (!mDrawables.contains(drawable)) { 192 // Make each drawable unique in the overlay; can't add it more than once 193 mDrawables.add(drawable); 194 invalidate(drawable.getBounds()); 195 drawable.setCallback(this); 196 } 197 } 198 199 public void remove(Drawable drawable) { 200 if (mDrawables != null) { 201 mDrawables.remove(drawable); 202 invalidate(drawable.getBounds()); 203 drawable.setCallback(null); 204 } 205 } 206 207 @Override 208 protected boolean verifyDrawable(Drawable who) { 209 return super.verifyDrawable(who) || (mDrawables != null && mDrawables.contains(who)); 210 } 211 212 public void add(View child) { 213 if (child.getParent() instanceof ViewGroup) { 214 ViewGroup parent = (ViewGroup) child.getParent(); 215 if (parent != mHostView && parent.getParent() != null) {// && 216// parent.isAttachedToWindow()) { 217 // Moving to different container; figure out how to position child such that 218 // it is in the same location on the screen 219 int[] parentLocation = new int[2]; 220 int[] hostViewLocation = new int[2]; 221 parent.getLocationOnScreen(parentLocation); 222 mHostView.getLocationOnScreen(hostViewLocation); 223 ViewCompat.offsetLeftAndRight(child, parentLocation[0] - hostViewLocation[0]); 224 ViewCompat.offsetTopAndBottom(child, parentLocation[1] - hostViewLocation[1]); 225 } 226 parent.removeView(child); 227// if (parent.getLayoutTransition() != null) { 228// // LayoutTransition will cause the child to delay removal - cancel it 229// parent.getLayoutTransition().cancel(LayoutTransition.DISAPPEARING); 230// } 231 // fail-safe if view is still attached for any reason 232 if (child.getParent() != null) { 233 parent.removeView(child); 234 } 235 } 236 super.addView(child, getChildCount() - 1); 237 } 238 239 public void remove(View view) { 240 super.removeView(view); 241 if (isEmpty()) { 242 mHostView.removeView(this); 243 } 244 } 245 246 public void clear() { 247 removeAllViews(); 248 if (mDrawables != null) { 249 mDrawables.clear(); 250 } 251 } 252 253 boolean isEmpty() { 254 if (getChildCount() == 0 && 255 (mDrawables == null || mDrawables.size() == 0)) { 256 return true; 257 } 258 return false; 259 } 260 261 @Override 262 public void invalidateDrawable(Drawable drawable) { 263 invalidate(drawable.getBounds()); 264 } 265 266 @Override 267 protected void dispatchDraw(Canvas canvas) { 268 int[] contentViewLocation = new int[2]; 269 int[] hostViewLocation = new int[2]; 270 ViewGroup parent = (ViewGroup) getParent(); 271 mHostView.getLocationOnScreen(contentViewLocation); 272 mRequestingView.getLocationOnScreen(hostViewLocation); 273 canvas.translate(hostViewLocation[0] - contentViewLocation[0], 274 hostViewLocation[1] - contentViewLocation[1]); 275 canvas.clipRect( 276 new Rect(0, 0, mRequestingView.getWidth(), mRequestingView.getHeight())); 277 super.dispatchDraw(canvas); 278 final int numDrawables = (mDrawables == null) ? 0 : mDrawables.size(); 279 for (int i = 0; i < numDrawables; ++i) { 280 mDrawables.get(i).draw(canvas); 281 } 282 } 283 284 @Override 285 protected void onLayout(boolean changed, int l, int t, int r, int b) { 286 // Noop: children are positioned absolutely 287 } 288 289 /* 290 The following invalidation overrides exist for the purpose of redirecting invalidation to 291 the host view. The overlay is not parented to the host view (since a View cannot be a 292 parent), so the invalidation cannot proceed through the normal parent hierarchy. 293 There is a built-in assumption that the overlay exactly covers the host view, therefore 294 the invalidation rectangles received do not need to be adjusted when forwarded to 295 the host view. 296 */ 297 298 private void getOffset(int[] offset) { 299 int[] contentViewLocation = new int[2]; 300 int[] hostViewLocation = new int[2]; 301 ViewGroup parent = (ViewGroup) getParent(); 302 mHostView.getLocationOnScreen(contentViewLocation); 303 mRequestingView.getLocationOnScreen(hostViewLocation); 304 offset[0] = hostViewLocation[0] - contentViewLocation[0]; 305 offset[1] = hostViewLocation[1] - contentViewLocation[1]; 306 } 307 308 public void invalidateChildFast(View child, final Rect dirty) { 309 if (mHostView != null) { 310 // Note: This is not a "fast" invalidation. Would be nice to instead invalidate 311 // using DisplayList properties and a dirty rect instead of causing a real 312 // invalidation of the host view 313 int left = child.getLeft(); 314 int top = child.getTop(); 315 int[] offset = new int[2]; 316 getOffset(offset); 317 // TODO: implement transforms 318// if (!child.getMatrix().isIdentity()) { 319// child.transformRect(dirty); 320// } 321 dirty.offset(left + offset[0], top + offset[1]); 322 mHostView.invalidate(dirty); 323 } 324 } 325 326 /** 327 * @hide 328 */ 329 @RestrictTo(GROUP_ID) 330 protected ViewParent invalidateChildInParentFast(int left, int top, Rect dirty) { 331 if (mHostView instanceof ViewGroup && sInvalidateChildInParentFastMethod != null) { 332 try { 333 int[] offset = new int[2]; 334 getOffset(offset); 335 sInvalidateChildInParentFastMethod.invoke(mHostView, left, top, dirty); 336 } catch (IllegalAccessException e) { 337 e.printStackTrace(); 338 } catch (InvocationTargetException e) { 339 e.printStackTrace(); 340 } 341 } 342 return null; 343 } 344 345 @Override 346 public ViewParent invalidateChildInParent(int[] location, Rect dirty) { 347 if (mHostView != null) { 348 dirty.offset(location[0], location[1]); 349 if (mHostView instanceof ViewGroup) { 350 location[0] = 0; 351 location[1] = 0; 352 int[] offset = new int[2]; 353 getOffset(offset); 354 dirty.offset(offset[0], offset[1]); 355 return super.invalidateChildInParent(location, dirty); 356// return ((ViewGroup) mHostView).invalidateChildInParent(location, dirty); 357 } else { 358 invalidate(dirty); 359 } 360 } 361 return null; 362 } 363 364 static class TouchInterceptor extends View { 365 TouchInterceptor(Context context) { 366 super(context); 367 } 368 } 369 } 370 371} 372