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