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