blob: 19b68e5c467fec17613ae92f27efb355d9816c56 [file] [log] [blame]
Stefan Kuhne61b47bb2015-07-28 14:04:25 -07001/*
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
17package com.android.internal.widget;
18
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070019import android.content.Context;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080020import android.graphics.Color;
21import android.graphics.Rect;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070022import android.os.RemoteException;
23import android.util.AttributeSet;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080024import android.util.Log;
25import android.view.GestureDetector;
Skuhne81c524a2015-08-12 13:34:14 -070026import android.view.MotionEvent;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070027import android.view.View;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080028import android.view.ViewConfiguration;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070029import android.view.ViewGroup;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070030import android.view.ViewOutlineProvider;
31import android.view.Window;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070032
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070033import com.android.internal.R;
34import com.android.internal.policy.PhoneWindow;
35
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080036import java.util.ArrayList;
37
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070038/**
Wale Ogunwale62a91d62015-11-18 11:44:10 -080039 * This class represents the special screen elements to control a window on freeform
40 * environment.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070041 * 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>
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080045 * </ul>
46 * After creating the view, the function {@link #setPhoneWindow} needs to be called to make
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070047 * the connection to it's owning PhoneWindow.
48 * Note: At this time the application can change various attributes of the DecorView which
Vladislav Kaznacheeva6cfcb32016-04-27 14:47:59 -070049 * will break things (in subtle/unexpected ways):
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070050 * <ul>
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070051 * <li>setOutlineProvider</li>
52 * <li>setSurfaceFormat</li>
53 * <li>..</li>
54 * </ul>
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080055 *
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>
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070075 */
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080076public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,
77 GestureDetector.OnGestureListener {
Wale Ogunwale62a91d62015-11-18 11:44:10 -080078 private final static String TAG = "DecorCaptionView";
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070079 private PhoneWindow mOwner = null;
Wale Ogunwale62a91d62015-11-18 11:44:10 -080080 private boolean mShow = false;
Skuhne81c524a2015-08-12 13:34:14 -070081
82 // True if the window is being dragged.
83 private boolean mDragging = false;
84
Filip Gruszczynski63250652015-11-18 14:43:01 -080085 private boolean mOverlayWithAppContent = false;
86
87 private View mCaption;
88 private View mContent;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080089 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;
Evan Rosky0d654cb2019-02-26 10:59:10 -0800106 private int mRootScrollY;
Filip Gruszczynski63250652015-11-18 14:43:01 -0800107
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800108 public DecorCaptionView(Context context) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700109 super(context);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800110 init(context);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700111 }
112
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800113 public DecorCaptionView(Context context, AttributeSet attrs) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700114 super(context, attrs);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800115 init(context);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700116 }
117
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800118 public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700119 super(context, attrs, defStyle);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800120 init(context);
121 }
122
123 private void init(Context context) {
124 mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
125 mGestureDetector = new GestureDetector(context, this);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700126 }
127
Filip Gruszczynski63250652015-11-18 14:43:01 -0800128 @Override
129 protected void onFinishInflate() {
130 super.onFinishInflate();
131 mCaption = getChildAt(0);
132 }
133
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800134 public void setPhoneWindow(PhoneWindow owner, boolean show) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700135 mOwner = owner;
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800136 mShow = show;
Andrii Kulian933076d2016-03-29 17:04:42 -0700137 mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800138 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 }
Skuhnef7b882c2015-08-11 17:18:58 -0700143 updateCaptionVisibility();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700144 // By changing the outline provider to BOUNDS, the window can remove its
145 // background without removing the shadow.
146 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800147 mMaximize = findViewById(R.id.maximize_window);
148 mClose = findViewById(R.id.close_window);
149 }
Skuhneb8160872015-09-22 09:51:39 -0700150
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800151 @Override
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();
Evan Rosky0d654cb2019-02-26 10:59:10 -0800158 // Only offset y for containment tests because the actual views are already translated.
159 if (mMaximizeRect.contains(x, y - mRootScrollY)) {
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800160 mClickTarget = mMaximize;
161 }
Evan Rosky0d654cb2019-02-26 10:59:10 -0800162 if (mCloseRect.contains(x, y - mRootScrollY)) {
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800163 mClickTarget = mClose;
164 }
165 }
166 return mClickTarget != null;
167 }
168
169 @Override
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;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700180 }
181
Skuhne81c524a2015-08-12 13:34:14 -0700182 @Override
Chong Zhang509ea6b2015-09-30 14:09:52 -0700183 public boolean onTouch(View v, MotionEvent e) {
Skuhne81c524a2015-08-12 13:34:14 -0700184 // 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.
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800187 final int x = (int) e.getX();
188 final int y = (int) e.getY();
Vladislav Kaznacheeva6cfcb32016-04-27 14:47:59 -0700189 final boolean fromMouse = e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE;
190 final boolean primaryButton = (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0;
Skuhne81c524a2015-08-12 13:34:14 -0700191 switch (e.getActionMasked()) {
192 case MotionEvent.ACTION_DOWN:
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800193 if (!mShow) {
194 // When there is no caption we should not react to anything.
Skuhnea635a262015-08-26 15:45:58 -0700195 return false;
196 }
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800197 // Checking for a drag action is started if we aren't dragging already and the
198 // starting event is either a left mouse button or any other input device.
Vladislav Kaznacheeva6cfcb32016-04-27 14:47:59 -0700199 if (!fromMouse || primaryButton) {
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800200 mCheckForDragging = true;
201 mTouchDownX = x;
202 mTouchDownY = y;
Skuhne81c524a2015-08-12 13:34:14 -0700203 }
204 break;
205
206 case MotionEvent.ACTION_MOVE:
Vladislav Kaznacheeva6cfcb32016-04-27 14:47:59 -0700207 if (!mDragging && mCheckForDragging && (fromMouse || passedSlop(x, y))) {
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800208 mCheckForDragging = false;
209 mDragging = true;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800210 startMovingTask(e.getRawX(), e.getRawY());
Vladislav Kaznacheeva6cfcb32016-04-27 14:47:59 -0700211 // After the above call the framework will take over the input.
212 // This handler will receive ACTION_CANCEL soon (possible after a few spurious
213 // ACTION_MOVE events which are safe to ignore).
Skuhne81c524a2015-08-12 13:34:14 -0700214 }
215 break;
216
217 case MotionEvent.ACTION_UP:
Skuhne81c524a2015-08-12 13:34:14 -0700218 case MotionEvent.ACTION_CANCEL:
Skuhnea5a93ee2015-08-20 15:43:57 -0700219 if (!mDragging) {
220 break;
Skuhne81c524a2015-08-12 13:34:14 -0700221 }
Skuhnea5a93ee2015-08-20 15:43:57 -0700222 // Abort the ongoing dragging.
223 mDragging = false;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800224 return !mCheckForDragging;
Skuhne81c524a2015-08-12 13:34:14 -0700225 }
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800226 return mDragging || mCheckForDragging;
227 }
228
229 @Override
230 public ArrayList<View> buildTouchDispatchChildList() {
231 mTouchDispatchList.ensureCapacity(3);
232 if (mCaption != null) {
233 mTouchDispatchList.add(mCaption);
234 }
235 if (mContent != null) {
236 mTouchDispatchList.add(mContent);
237 }
238 return mTouchDispatchList;
239 }
240
Andrii Kuliana6aea982016-07-26 18:42:46 -0700241 @Override
242 public boolean shouldDelayChildPressedState() {
243 return false;
244 }
245
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800246 private boolean passedSlop(int x, int y) {
247 return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop;
Skuhne81c524a2015-08-12 13:34:14 -0700248 }
249
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700250 /**
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800251 * The phone window configuration has changed and the caption needs to be updated.
252 * @param show True if the caption should be shown.
Wale Ogunwale2b547c32015-11-18 10:33:22 -0800253 */
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800254 public void onConfigurationChanged(boolean show) {
255 mShow = show;
Skuhnef7b882c2015-08-11 17:18:58 -0700256 updateCaptionVisibility();
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700257 }
258
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700259 @Override
Skuhnef7b882c2015-08-11 17:18:58 -0700260 public void addView(View child, int index, ViewGroup.LayoutParams params) {
Filip Gruszczynski63250652015-11-18 14:43:01 -0800261 if (!(params instanceof MarginLayoutParams)) {
262 throw new IllegalArgumentException(
263 "params " + params + " must subclass MarginLayoutParams");
264 }
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700265 // Make sure that we never get more then one client area in our view.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700266 if (index >= 2 || getChildCount() >= 2) {
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800267 throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700268 }
Filip Gruszczynski63250652015-11-18 14:43:01 -0800269 // To support the overlaying content in the caption, we need to put the content view as the
270 // first child to get the right Z-Ordering.
271 super.addView(child, 0, params);
272 mContent = child;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700273 }
274
Filip Gruszczynski63250652015-11-18 14:43:01 -0800275 @Override
276 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
277 final int captionHeight;
278 if (mCaption.getVisibility() != View.GONE) {
279 measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0);
280 captionHeight = mCaption.getMeasuredHeight();
281 } else {
282 captionHeight = 0;
283 }
284 if (mContent != null) {
285 if (mOverlayWithAppContent) {
286 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
287 } else {
288 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec,
289 captionHeight);
290 }
291 }
292
293 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
294 MeasureSpec.getSize(heightMeasureSpec));
295 }
296
297 @Override
298 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
299 final int captionHeight;
300 if (mCaption.getVisibility() != View.GONE) {
301 mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
302 captionHeight = mCaption.getBottom() - mCaption.getTop();
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800303 mMaximize.getHitRect(mMaximizeRect);
304 mClose.getHitRect(mCloseRect);
Filip Gruszczynski63250652015-11-18 14:43:01 -0800305 } else {
306 captionHeight = 0;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800307 mMaximizeRect.setEmpty();
308 mCloseRect.setEmpty();
Filip Gruszczynski63250652015-11-18 14:43:01 -0800309 }
310
311 if (mContent != null) {
312 if (mOverlayWithAppContent) {
313 mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
314 } else {
315 mContent.layout(0, captionHeight, mContent.getMeasuredWidth(),
316 captionHeight + mContent.getMeasuredHeight());
317 }
318 }
Filip Gruszczynski3dec0812015-12-09 08:42:41 -0800319
320 // This assumes that the caption bar is at the top.
321 mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
322 mClose.getRight(), mClose.getBottom());
Filip Gruszczynski63250652015-11-18 14:43:01 -0800323 }
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700324
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700325 /**
Skuhnef7b882c2015-08-11 17:18:58 -0700326 * Updates the visibility of the caption.
327 **/
328 private void updateCaptionVisibility() {
Garfield Tan3b9613c2018-12-26 17:08:51 -0800329 mCaption.setVisibility(mShow ? VISIBLE : GONE);
Filip Gruszczynski63250652015-11-18 14:43:01 -0800330 mCaption.setOnTouchListener(this);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700331 }
Stefan Kuhne1b420572015-08-07 10:50:19 -0700332
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700333 /**
Yunfan Chend967af82019-01-17 18:30:18 +0900334 * Maximize or restore the window by moving it to the maximized or freeform workspace stack.
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700335 **/
Yunfan Chend967af82019-01-17 18:30:18 +0900336 private void toggleFreeformWindowingMode() {
Skuhnece2faa52015-08-11 10:36:38 -0700337 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
Stefan Kuhne1b420572015-08-07 10:50:19 -0700338 if (callback != null) {
339 try {
Yunfan Chend967af82019-01-17 18:30:18 +0900340 callback.toggleFreeformWindowingMode();
Stefan Kuhne1b420572015-08-07 10:50:19 -0700341 } catch (RemoteException ex) {
342 Log.e(TAG, "Cannot change task workspace.");
343 }
344 }
345 }
Wale Ogunwale8cc5a742015-11-17 15:41:05 -0800346
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800347 public boolean isCaptionShowing() {
348 return mShow;
Wale Ogunwale8cc5a742015-11-17 15:41:05 -0800349 }
350
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800351 public int getCaptionHeight() {
Filip Gruszczynski63250652015-11-18 14:43:01 -0800352 return (mCaption != null) ? mCaption.getHeight() : 0;
353 }
354
355 public void removeContentView() {
356 if (mContent != null) {
357 removeView(mContent);
358 mContent = null;
359 }
360 }
361
362 public View getCaption() {
363 return mCaption;
364 }
365
366 @Override
367 public LayoutParams generateLayoutParams(AttributeSet attrs) {
368 return new MarginLayoutParams(getContext(), attrs);
369 }
370
371 @Override
372 protected LayoutParams generateDefaultLayoutParams() {
373 return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
374 MarginLayoutParams.MATCH_PARENT);
375 }
376
377 @Override
378 protected LayoutParams generateLayoutParams(LayoutParams p) {
379 return new MarginLayoutParams(p);
380 }
381
382 @Override
383 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
384 return p instanceof MarginLayoutParams;
Wale Ogunwale8cc5a742015-11-17 15:41:05 -0800385 }
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800386
387 @Override
388 public boolean onDown(MotionEvent e) {
389 return false;
390 }
391
392 @Override
393 public void onShowPress(MotionEvent e) {
394
395 }
396
397 @Override
398 public boolean onSingleTapUp(MotionEvent e) {
399 if (mClickTarget == mMaximize) {
Yunfan Chend967af82019-01-17 18:30:18 +0900400 toggleFreeformWindowingMode();
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800401 } else if (mClickTarget == mClose) {
Ned Burns7d6cb912016-12-02 17:25:33 -0500402 mOwner.dispatchOnWindowDismissed(
403 true /*finishTask*/, false /*suppressWindowTransition*/);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800404 }
405 return true;
406 }
407
408 @Override
409 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
410 return false;
411 }
412
413 @Override
414 public void onLongPress(MotionEvent e) {
415
416 }
417
418 @Override
419 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
420 return false;
421 }
Evan Rosky0d654cb2019-02-26 10:59:10 -0800422
423 /**
424 * Called when {@link android.view.ViewRootImpl} scrolls for adjustPan.
425 */
426 public void onRootViewScrollYChanged(int scrollY) {
427 // Offset the caption opposite the root scroll. This keeps the caption at the
428 // top of the window during adjustPan.
429 if (mCaption != null) {
430 mRootScrollY = scrollY;
431 mCaption.setTranslationY(scrollY);
432 }
433 }
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700434}