blob: 21021457377a4cdb04349384667a32eabd1576c3 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.widget;
import android.content.Context;
import android.graphics.Rect;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.Window;
import com.android.internal.R;
import com.android.internal.policy.DecorView;
import com.android.internal.policy.PhoneWindow;
import java.util.ArrayList;
/**
* This class represents the special screen elements to control a window on freeform
* environment.
* As such this class handles the following things:
* <ul>
* <li>The caption, containing the system buttons like maximize, close and such as well as
* allowing the user to drag the window around.</li>
* </ul>
* After creating the view, the function {@link #setPhoneWindow} needs to be called to make
* the connection to it's owning PhoneWindow.
* Note: At this time the application can change various attributes of the DecorView which
* will break things (in subtle/unexpected ways):
* <ul>
* <li>setOutlineProvider</li>
* <li>setSurfaceFormat</li>
* <li>..</li>
* </ul>
*
* Here describe the behavior of overlaying caption on the content and drawing.
*
* First, no matter where the content View gets added, it will always be the first child and the
* caption will be the second. This way the caption will always be drawn on top of the content when
* overlaying is enabled.
*
* Second, the touch dispatch is customized to handle overlaying. This is what happens when touch
* is dispatched on the caption area while overlaying it on content:
* <ul>
* <li>DecorCaptionView.onInterceptTouchEvent() will try intercepting the touch events if the
* down action is performed on top close or maximize buttons; the reason for that is we want these
* buttons to always work.</li>
* <li>The caption view will try to consume the event to apply the dragging logic.</li>
* <li>If the touch event is not consumed by the caption, the content View will receive the touch
* event</li>
* </ul>
*/
public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,
GestureDetector.OnGestureListener {
private final static String TAG = "DecorCaptionView";
private PhoneWindow mOwner = null;
private boolean mShow = false;
// True if the window is being dragged.
private boolean mDragging = false;
private boolean mOverlayWithAppContent = false;
private View mCaption;
private View mContent;
private View mMaximize;
private View mClose;
// Fields for detecting drag events.
private int mTouchDownX;
private int mTouchDownY;
private boolean mCheckForDragging;
private int mDragSlop;
// Fields for detecting and intercepting click events on close/maximize.
private ArrayList<View> mTouchDispatchList = new ArrayList<>(2);
// We use the gesture detector to detect clicks on close/maximize buttons and to be consistent
// with existing click detection.
private GestureDetector mGestureDetector;
private final Rect mCloseRect = new Rect();
private final Rect mMaximizeRect = new Rect();
private View mClickTarget;
private int mRootScrollY;
public DecorCaptionView(Context context) {
super(context);
init(context);
}
public DecorCaptionView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mGestureDetector = new GestureDetector(context, this);
setContentDescription(context.getString(R.string.accessibility_freeform_caption,
context.getPackageManager().getApplicationLabel(context.getApplicationInfo())));
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mCaption = getChildAt(0);
}
public void setPhoneWindow(PhoneWindow owner, boolean show) {
mOwner = owner;
mShow = show;
mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
updateCaptionVisibility();
// By changing the outline provider to BOUNDS, the window can remove its
// background without removing the shadow.
mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
mMaximize = findViewById(R.id.maximize_window);
mClose = findViewById(R.id.close_window);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// If the user starts touch on the maximize/close buttons, we immediately intercept, so
// that these buttons are always clickable.
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
final int x = (int) ev.getX();
final int y = (int) ev.getY();
// Only offset y for containment tests because the actual views are already translated.
if (mMaximizeRect.contains(x, y - mRootScrollY)) {
mClickTarget = mMaximize;
}
if (mCloseRect.contains(x, y - mRootScrollY)) {
mClickTarget = mClose;
}
}
return mClickTarget != null;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mClickTarget != null) {
mGestureDetector.onTouchEvent(event);
final int action = event.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mClickTarget = null;
}
return true;
}
return false;
}
@Override
public boolean onTouch(View v, MotionEvent e) {
// Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
// the old input device events get cancelled first. So no need to remember the kind of
// input device we are listening to.
final int x = (int) e.getX();
final int y = (int) e.getY();
final boolean fromMouse = e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE;
final boolean primaryButton = (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0;
final int actionMasked = e.getActionMasked();
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
if (!mShow) {
// When there is no caption we should not react to anything.
return false;
}
// Checking for a drag action is started if we aren't dragging already and the
// starting event is either a left mouse button or any other input device.
if (!fromMouse || primaryButton) {
mCheckForDragging = true;
mTouchDownX = x;
mTouchDownY = y;
}
break;
case MotionEvent.ACTION_MOVE:
if (!mDragging && mCheckForDragging && (fromMouse || passedSlop(x, y))) {
mCheckForDragging = false;
mDragging = true;
startMovingTask(e.getRawX(), e.getRawY());
// After the above call the framework will take over the input.
// This handler will receive ACTION_CANCEL soon (possible after a few spurious
// ACTION_MOVE events which are safe to ignore).
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (!mDragging) {
break;
}
// Abort the ongoing dragging.
if (actionMasked == MotionEvent.ACTION_UP) {
// If it receives ACTION_UP event, the dragging is already finished and also
// the system can not end drag on ACTION_UP event. So request to finish
// dragging.
finishMovingTask();
}
mDragging = false;
return !mCheckForDragging;
}
return mDragging || mCheckForDragging;
}
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
private boolean passedSlop(int x, int y) {
return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop;
}
/**
* The phone window configuration has changed and the caption needs to be updated.
* @param show True if the caption should be shown.
*/
public void onConfigurationChanged(boolean show) {
mShow = show;
updateCaptionVisibility();
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (!(params instanceof MarginLayoutParams)) {
throw new IllegalArgumentException(
"params " + params + " must subclass MarginLayoutParams");
}
// Make sure that we never get more then one client area in our view.
if (index >= 2 || getChildCount() >= 2) {
throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
}
// To support the overlaying content in the caption, we need to put the content view as the
// first child to get the right Z-Ordering.
super.addView(child, 0, params);
mContent = child;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int captionHeight;
if (mCaption.getVisibility() != View.GONE) {
measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0);
captionHeight = mCaption.getMeasuredHeight();
} else {
captionHeight = 0;
}
if (mContent != null) {
if (mOverlayWithAppContent) {
measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
} else {
measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec,
captionHeight);
}
}
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int captionHeight;
if (mCaption.getVisibility() != View.GONE) {
mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
captionHeight = mCaption.getBottom() - mCaption.getTop();
mMaximize.getHitRect(mMaximizeRect);
mClose.getHitRect(mCloseRect);
} else {
captionHeight = 0;
mMaximizeRect.setEmpty();
mCloseRect.setEmpty();
}
if (mContent != null) {
if (mOverlayWithAppContent) {
mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
} else {
mContent.layout(0, captionHeight, mContent.getMeasuredWidth(),
captionHeight + mContent.getMeasuredHeight());
}
}
((DecorView) mOwner.getDecorView()).notifyCaptionHeightChanged();
// This assumes that the caption bar is at the top.
mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
mClose.getRight(), mClose.getBottom());
}
/**
* Updates the visibility of the caption.
**/
private void updateCaptionVisibility() {
mCaption.setVisibility(mShow ? VISIBLE : GONE);
mCaption.setOnTouchListener(this);
}
/**
* Maximize or restore the window by moving it to the maximized or freeform workspace stack.
**/
private void toggleFreeformWindowingMode() {
Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
if (callback != null) {
try {
callback.toggleFreeformWindowingMode();
} catch (RemoteException ex) {
Log.e(TAG, "Cannot change task workspace.");
}
}
}
public boolean isCaptionShowing() {
return mShow;
}
public int getCaptionHeight() {
return (mCaption != null) ? mCaption.getHeight() : 0;
}
public void removeContentView() {
if (mContent != null) {
removeView(mContent);
mContent = null;
}
}
public View getCaption() {
return mCaption;
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
MarginLayoutParams.MATCH_PARENT);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof MarginLayoutParams;
}
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (mClickTarget == mMaximize) {
toggleFreeformWindowingMode();
} else if (mClickTarget == mClose) {
mOwner.dispatchOnWindowDismissed(
true /*finishTask*/, false /*suppressWindowTransition*/);
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
/**
* Called when {@link android.view.ViewRootImpl} scrolls for adjustPan.
*/
public void onRootViewScrollYChanged(int scrollY) {
// Offset the caption opposite the root scroll. This keeps the caption at the
// top of the window during adjustPan.
if (mCaption != null) {
mRootScrollY = scrollY;
mCaption.setTranslationY(scrollY);
}
}
}