1 /* 2 * Copyright (C) 2015 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 com.android.internal.widget; 18 19 import android.content.Context; 20 import android.graphics.Rect; 21 import android.util.AttributeSet; 22 import android.view.GestureDetector; 23 import android.view.MotionEvent; 24 import android.view.View; 25 import android.view.ViewConfiguration; 26 import android.view.ViewGroup; 27 import android.view.ViewOutlineProvider; 28 import android.view.Window; 29 30 import com.android.internal.R; 31 import com.android.internal.policy.DecorView; 32 import com.android.internal.policy.PhoneWindow; 33 34 import java.util.ArrayList; 35 36 /** 37 * This class represents the special screen elements to control a window on freeform 38 * environment. 39 * As such this class handles the following things: 40 * <ul> 41 * <li>The caption, containing the system buttons like maximize, close and such as well as 42 * allowing the user to drag the window around.</li> 43 * </ul> 44 * After creating the view, the function {@link #setPhoneWindow} needs to be called to make 45 * the connection to it's owning PhoneWindow. 46 * Note: At this time the application can change various attributes of the DecorView which 47 * will break things (in subtle/unexpected ways): 48 * <ul> 49 * <li>setOutlineProvider</li> 50 * <li>setSurfaceFormat</li> 51 * <li>..</li> 52 * </ul> 53 * 54 * Here describe the behavior of overlaying caption on the content and drawing. 55 * 56 * First, no matter where the content View gets added, it will always be the first child and the 57 * caption will be the second. This way the caption will always be drawn on top of the content when 58 * overlaying is enabled. 59 * 60 * Second, the touch dispatch is customized to handle overlaying. This is what happens when touch 61 * is dispatched on the caption area while overlaying it on content: 62 * <ul> 63 * <li>DecorCaptionView.onInterceptTouchEvent() will try intercepting the touch events if the 64 * down action is performed on top close or maximize buttons; the reason for that is we want these 65 * buttons to always work.</li> 66 * <li>The caption view will try to consume the event to apply the dragging logic.</li> 67 * <li>If the touch event is not consumed by the caption, the content View will receive the touch 68 * event</li> 69 * </ul> 70 */ 71 public class DecorCaptionView extends ViewGroup implements View.OnTouchListener, 72 GestureDetector.OnGestureListener { 73 private PhoneWindow mOwner = null; 74 private boolean mShow = false; 75 76 // True if the window is being dragged. 77 private boolean mDragging = false; 78 79 private boolean mOverlayWithAppContent = false; 80 81 private View mCaption; 82 private View mContent; 83 private View mMaximize; 84 private View mClose; 85 86 // Fields for detecting drag events. 87 private int mTouchDownX; 88 private int mTouchDownY; 89 private boolean mCheckForDragging; 90 private int mDragSlop; 91 92 // Fields for detecting and intercepting click events on close/maximize. 93 private ArrayList<View> mTouchDispatchList = new ArrayList<>(2); 94 // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent 95 // with existing click detection. 96 private GestureDetector mGestureDetector; 97 private final Rect mCloseRect = new Rect(); 98 private final Rect mMaximizeRect = new Rect(); 99 private View mClickTarget; 100 private int mRootScrollY; 101 DecorCaptionView(Context context)102 public DecorCaptionView(Context context) { 103 super(context); 104 init(context); 105 } 106 DecorCaptionView(Context context, AttributeSet attrs)107 public DecorCaptionView(Context context, AttributeSet attrs) { 108 super(context, attrs); 109 init(context); 110 } 111 DecorCaptionView(Context context, AttributeSet attrs, int defStyle)112 public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) { 113 super(context, attrs, defStyle); 114 init(context); 115 } 116 init(Context context)117 private void init(Context context) { 118 mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 119 mGestureDetector = new GestureDetector(context, this); 120 setContentDescription(context.getString(R.string.accessibility_freeform_caption, 121 context.getPackageManager().getApplicationLabel(context.getApplicationInfo()))); 122 } 123 124 @Override onFinishInflate()125 protected void onFinishInflate() { 126 super.onFinishInflate(); 127 mCaption = getChildAt(0); 128 } 129 setPhoneWindow(PhoneWindow owner, boolean show)130 public void setPhoneWindow(PhoneWindow owner, boolean show) { 131 mOwner = owner; 132 mShow = show; 133 mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled(); 134 updateCaptionVisibility(); 135 // By changing the outline provider to BOUNDS, the window can remove its 136 // background without removing the shadow. 137 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS); 138 mMaximize = findViewById(R.id.maximize_window); 139 mClose = findViewById(R.id.close_window); 140 } 141 142 @Override onInterceptTouchEvent(MotionEvent ev)143 public boolean onInterceptTouchEvent(MotionEvent ev) { 144 // If the user starts touch on the maximize/close buttons, we immediately intercept, so 145 // that these buttons are always clickable. 146 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 147 final int x = (int) ev.getX(); 148 final int y = (int) ev.getY(); 149 // Only offset y for containment tests because the actual views are already translated. 150 if (mMaximizeRect.contains(x, y - mRootScrollY)) { 151 mClickTarget = mMaximize; 152 } 153 if (mCloseRect.contains(x, y - mRootScrollY)) { 154 mClickTarget = mClose; 155 } 156 } 157 return mClickTarget != null; 158 } 159 160 @Override onTouchEvent(MotionEvent event)161 public boolean onTouchEvent(MotionEvent event) { 162 if (mClickTarget != null) { 163 mGestureDetector.onTouchEvent(event); 164 final int action = event.getAction(); 165 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 166 mClickTarget = null; 167 } 168 return true; 169 } 170 return false; 171 } 172 173 @Override onTouch(View v, MotionEvent e)174 public boolean onTouch(View v, MotionEvent e) { 175 // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch) 176 // the old input device events get cancelled first. So no need to remember the kind of 177 // input device we are listening to. 178 final int x = (int) e.getX(); 179 final int y = (int) e.getY(); 180 final boolean fromMouse = e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE; 181 final boolean primaryButton = (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0; 182 final int actionMasked = e.getActionMasked(); 183 switch (actionMasked) { 184 case MotionEvent.ACTION_DOWN: 185 if (!mShow) { 186 // When there is no caption we should not react to anything. 187 return false; 188 } 189 // Checking for a drag action is started if we aren't dragging already and the 190 // starting event is either a left mouse button or any other input device. 191 if (!fromMouse || primaryButton) { 192 mCheckForDragging = true; 193 mTouchDownX = x; 194 mTouchDownY = y; 195 } 196 break; 197 198 case MotionEvent.ACTION_MOVE: 199 if (!mDragging && mCheckForDragging && (fromMouse || passedSlop(x, y))) { 200 mCheckForDragging = false; 201 mDragging = true; 202 startMovingTask(e.getRawX(), e.getRawY()); 203 // After the above call the framework will take over the input. 204 // This handler will receive ACTION_CANCEL soon (possible after a few spurious 205 // ACTION_MOVE events which are safe to ignore). 206 } 207 break; 208 209 case MotionEvent.ACTION_UP: 210 case MotionEvent.ACTION_CANCEL: 211 if (!mDragging) { 212 break; 213 } 214 // Abort the ongoing dragging. 215 if (actionMasked == MotionEvent.ACTION_UP) { 216 // If it receives ACTION_UP event, the dragging is already finished and also 217 // the system can not end drag on ACTION_UP event. So request to finish 218 // dragging. 219 finishMovingTask(); 220 } 221 mDragging = false; 222 return !mCheckForDragging; 223 } 224 return mDragging || mCheckForDragging; 225 } 226 227 @Override shouldDelayChildPressedState()228 public boolean shouldDelayChildPressedState() { 229 return false; 230 } 231 passedSlop(int x, int y)232 private boolean passedSlop(int x, int y) { 233 return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop; 234 } 235 236 /** 237 * The phone window configuration has changed and the caption needs to be updated. 238 * @param show True if the caption should be shown. 239 */ onConfigurationChanged(boolean show)240 public void onConfigurationChanged(boolean show) { 241 mShow = show; 242 updateCaptionVisibility(); 243 } 244 245 @Override addView(View child, int index, ViewGroup.LayoutParams params)246 public void addView(View child, int index, ViewGroup.LayoutParams params) { 247 if (!(params instanceof MarginLayoutParams)) { 248 throw new IllegalArgumentException( 249 "params " + params + " must subclass MarginLayoutParams"); 250 } 251 // Make sure that we never get more then one client area in our view. 252 if (index >= 2 || getChildCount() >= 2) { 253 throw new IllegalStateException("DecorCaptionView can only handle 1 client view"); 254 } 255 // To support the overlaying content in the caption, we need to put the content view as the 256 // first child to get the right Z-Ordering. 257 super.addView(child, 0, params); 258 mContent = child; 259 } 260 261 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)262 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 263 final int captionHeight; 264 if (mCaption.getVisibility() != View.GONE) { 265 measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0); 266 captionHeight = mCaption.getMeasuredHeight(); 267 } else { 268 captionHeight = 0; 269 } 270 if (mContent != null) { 271 if (mOverlayWithAppContent) { 272 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0); 273 } else { 274 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 275 captionHeight); 276 } 277 } 278 279 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 280 MeasureSpec.getSize(heightMeasureSpec)); 281 } 282 283 @Override onLayout(boolean changed, int left, int top, int right, int bottom)284 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 285 final int captionHeight; 286 if (mCaption.getVisibility() != View.GONE) { 287 mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight()); 288 captionHeight = mCaption.getBottom() - mCaption.getTop(); 289 mMaximize.getHitRect(mMaximizeRect); 290 mClose.getHitRect(mCloseRect); 291 } else { 292 captionHeight = 0; 293 mMaximizeRect.setEmpty(); 294 mCloseRect.setEmpty(); 295 } 296 297 if (mContent != null) { 298 if (mOverlayWithAppContent) { 299 mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight()); 300 } else { 301 mContent.layout(0, captionHeight, mContent.getMeasuredWidth(), 302 captionHeight + mContent.getMeasuredHeight()); 303 } 304 } 305 306 ((DecorView) mOwner.getDecorView()).notifyCaptionHeightChanged(); 307 308 // This assumes that the caption bar is at the top. 309 mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(), 310 mClose.getRight(), mClose.getBottom()); 311 } 312 313 /** 314 * Updates the visibility of the caption. 315 **/ updateCaptionVisibility()316 private void updateCaptionVisibility() { 317 mCaption.setVisibility(mShow ? VISIBLE : GONE); 318 mCaption.setOnTouchListener(this); 319 } 320 321 /** 322 * Maximize or restore the window by moving it to the maximized or freeform workspace stack. 323 **/ toggleFreeformWindowingMode()324 private void toggleFreeformWindowingMode() { 325 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback(); 326 if (callback != null) { 327 callback.toggleFreeformWindowingMode(); 328 } 329 } 330 isCaptionShowing()331 public boolean isCaptionShowing() { 332 return mShow; 333 } 334 getCaptionHeight()335 public int getCaptionHeight() { 336 return (mCaption != null) ? mCaption.getHeight() : 0; 337 } 338 removeContentView()339 public void removeContentView() { 340 if (mContent != null) { 341 removeView(mContent); 342 mContent = null; 343 } 344 } 345 getCaption()346 public View getCaption() { 347 return mCaption; 348 } 349 350 @Override generateLayoutParams(AttributeSet attrs)351 public LayoutParams generateLayoutParams(AttributeSet attrs) { 352 return new MarginLayoutParams(getContext(), attrs); 353 } 354 355 @Override generateDefaultLayoutParams()356 protected LayoutParams generateDefaultLayoutParams() { 357 return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT, 358 MarginLayoutParams.MATCH_PARENT); 359 } 360 361 @Override generateLayoutParams(LayoutParams p)362 protected LayoutParams generateLayoutParams(LayoutParams p) { 363 return new MarginLayoutParams(p); 364 } 365 366 @Override checkLayoutParams(ViewGroup.LayoutParams p)367 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 368 return p instanceof MarginLayoutParams; 369 } 370 371 @Override onDown(MotionEvent e)372 public boolean onDown(MotionEvent e) { 373 return false; 374 } 375 376 @Override onShowPress(MotionEvent e)377 public void onShowPress(MotionEvent e) { 378 379 } 380 381 @Override onSingleTapUp(MotionEvent e)382 public boolean onSingleTapUp(MotionEvent e) { 383 if (mClickTarget == mMaximize) { 384 toggleFreeformWindowingMode(); 385 } else if (mClickTarget == mClose) { 386 mOwner.dispatchOnWindowDismissed( 387 true /*finishTask*/, false /*suppressWindowTransition*/); 388 } 389 return true; 390 } 391 392 @Override onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)393 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 394 return false; 395 } 396 397 @Override onLongPress(MotionEvent e)398 public void onLongPress(MotionEvent e) { 399 400 } 401 402 @Override onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)403 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 404 return false; 405 } 406 407 /** 408 * Called when {@link android.view.ViewRootImpl} scrolls for adjustPan. 409 */ onRootViewScrollYChanged(int scrollY)410 public void onRootViewScrollYChanged(int scrollY) { 411 // Offset the caption opposite the root scroll. This keeps the caption at the 412 // top of the window during adjustPan. 413 if (mCaption != null) { 414 mRootScrollY = scrollY; 415 mCaption.setTranslationY(scrollY); 416 } 417 } 418 } 419