2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.internal.widget;
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;
33 import com.android.internal.R;
34 import com.android.internal.policy.PhoneWindow;
36 import java.util.ArrayList;
39 * This class represents the special screen elements to control a window on freeform
41 * As such this class handles the following things:
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>
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):
51 * <li>setOutlineProvider</li>
52 * <li>setSurfaceFormat</li>
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.
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.
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:
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>
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;
82 // True if the window is being dragged.
83 private boolean mDragging = false;
85 private boolean mOverlayWithAppContent = false;
87 private View mCaption;
88 private View mContent;
89 private View mMaximize;
92 // Fields for detecting drag events.
93 private int mTouchDownX;
94 private int mTouchDownY;
95 private boolean mCheckForDragging;
96 private int mDragSlop;
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;
107 public DecorCaptionView(Context context) {
112 public DecorCaptionView(Context context, AttributeSet attrs) {
113 super(context, attrs);
117 public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
118 super(context, attrs, defStyle);
122 private void init(Context context) {
123 mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
124 mGestureDetector = new GestureDetector(context, this);
128 protected void onFinishInflate() {
129 super.onFinishInflate();
130 mCaption = getChildAt(0);
133 public void setPhoneWindow(PhoneWindow owner, boolean show) {
136 mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
137 if (mOverlayWithAppContent) {
138 // The caption is covering the content, so we make its background transparent to make
139 // the content visible.
140 mCaption.setBackgroundColor(Color.TRANSPARENT);
142 updateCaptionVisibility();
143 // By changing the outline provider to BOUNDS, the window can remove its
144 // background without removing the shadow.
145 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
146 mMaximize = findViewById(R.id.maximize_window);
147 mClose = findViewById(R.id.close_window);
151 public boolean onInterceptTouchEvent(MotionEvent ev) {
152 // If the user starts touch on the maximize/close buttons, we immediately intercept, so
153 // that these buttons are always clickable.
154 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
155 final int x = (int) ev.getX();
156 final int y = (int) ev.getY();
157 if (mMaximizeRect.contains(x, y)) {
158 mClickTarget = mMaximize;
160 if (mCloseRect.contains(x, y)) {
161 mClickTarget = mClose;
164 return mClickTarget != null;
168 public boolean onTouchEvent(MotionEvent event) {
169 if (mClickTarget != null) {
170 mGestureDetector.onTouchEvent(event);
171 final int action = event.getAction();
172 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
181 public boolean onTouch(View v, MotionEvent e) {
182 // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
183 // the old input device events get cancelled first. So no need to remember the kind of
184 // input device we are listening to.
185 final int x = (int) e.getX();
186 final int y = (int) e.getY();
187 final boolean fromMouse = e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE;
188 final boolean primaryButton = (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0;
189 switch (e.getActionMasked()) {
190 case MotionEvent.ACTION_DOWN:
192 // When there is no caption we should not react to anything.
195 // Checking for a drag action is started if we aren't dragging already and the
196 // starting event is either a left mouse button or any other input device.
197 if (!fromMouse || primaryButton) {
198 mCheckForDragging = true;
204 case MotionEvent.ACTION_MOVE:
205 if (!mDragging && mCheckForDragging && (fromMouse || passedSlop(x, y))) {
206 mCheckForDragging = false;
208 startMovingTask(e.getRawX(), e.getRawY());
209 // After the above call the framework will take over the input.
210 // This handler will receive ACTION_CANCEL soon (possible after a few spurious
211 // ACTION_MOVE events which are safe to ignore).
215 case MotionEvent.ACTION_UP:
216 case MotionEvent.ACTION_CANCEL:
220 // Abort the ongoing dragging.
222 return !mCheckForDragging;
224 return mDragging || mCheckForDragging;
228 public ArrayList<View> buildTouchDispatchChildList() {
229 mTouchDispatchList.ensureCapacity(3);
230 if (mCaption != null) {
231 mTouchDispatchList.add(mCaption);
233 if (mContent != null) {
234 mTouchDispatchList.add(mContent);
236 return mTouchDispatchList;
240 public boolean shouldDelayChildPressedState() {
244 private boolean passedSlop(int x, int y) {
245 return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop;
249 * The phone window configuration has changed and the caption needs to be updated.
250 * @param show True if the caption should be shown.
252 public void onConfigurationChanged(boolean show) {
254 updateCaptionVisibility();
258 public void addView(View child, int index, ViewGroup.LayoutParams params) {
259 if (!(params instanceof MarginLayoutParams)) {
260 throw new IllegalArgumentException(
261 "params " + params + " must subclass MarginLayoutParams");
263 // Make sure that we never get more then one client area in our view.
264 if (index >= 2 || getChildCount() >= 2) {
265 throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
267 // To support the overlaying content in the caption, we need to put the content view as the
268 // first child to get the right Z-Ordering.
269 super.addView(child, 0, params);
274 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
275 final int captionHeight;
276 if (mCaption.getVisibility() != View.GONE) {
277 measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0);
278 captionHeight = mCaption.getMeasuredHeight();
282 if (mContent != null) {
283 if (mOverlayWithAppContent) {
284 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
286 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec,
291 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
292 MeasureSpec.getSize(heightMeasureSpec));
296 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
297 final int captionHeight;
298 if (mCaption.getVisibility() != View.GONE) {
299 mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
300 captionHeight = mCaption.getBottom() - mCaption.getTop();
301 mMaximize.getHitRect(mMaximizeRect);
302 mClose.getHitRect(mCloseRect);
305 mMaximizeRect.setEmpty();
306 mCloseRect.setEmpty();
309 if (mContent != null) {
310 if (mOverlayWithAppContent) {
311 mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
313 mContent.layout(0, captionHeight, mContent.getMeasuredWidth(),
314 captionHeight + mContent.getMeasuredHeight());
318 // This assumes that the caption bar is at the top.
319 mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
320 mClose.getRight(), mClose.getBottom());
323 * Determine if the workspace is entirely covered by the window.
324 * @return Returns true when the window is filling the entire screen/workspace.
326 private boolean isFillingScreen() {
327 return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
328 (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
329 View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
333 * Updates the visibility of the caption.
335 private void updateCaptionVisibility() {
336 // Don't show the caption if the window has e.g. entered full screen.
337 boolean invisible = isFillingScreen() || !mShow;
338 mCaption.setVisibility(invisible ? GONE : VISIBLE);
339 mCaption.setOnTouchListener(this);
343 * Maximize the window by moving it to the maximized workspace stack.
345 private void maximizeWindow() {
346 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
347 if (callback != null) {
349 callback.exitFreeformMode();
350 } catch (RemoteException ex) {
351 Log.e(TAG, "Cannot change task workspace.");
356 public boolean isCaptionShowing() {
360 public int getCaptionHeight() {
361 return (mCaption != null) ? mCaption.getHeight() : 0;
364 public void removeContentView() {
365 if (mContent != null) {
366 removeView(mContent);
371 public View getCaption() {
376 public LayoutParams generateLayoutParams(AttributeSet attrs) {
377 return new MarginLayoutParams(getContext(), attrs);
381 protected LayoutParams generateDefaultLayoutParams() {
382 return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
383 MarginLayoutParams.MATCH_PARENT);
387 protected LayoutParams generateLayoutParams(LayoutParams p) {
388 return new MarginLayoutParams(p);
392 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
393 return p instanceof MarginLayoutParams;
397 public boolean onDown(MotionEvent e) {
402 public void onShowPress(MotionEvent e) {
407 public boolean onSingleTapUp(MotionEvent e) {
408 if (mClickTarget == mMaximize) {
410 } else if (mClickTarget == mClose) {
411 mOwner.dispatchOnWindowDismissed(
412 true /*finishTask*/, false /*suppressWindowTransition*/);
418 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
423 public void onLongPress(MotionEvent e) {
428 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {