| /* |
| * 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.Color; |
| 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.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> |
| * |
| * Although this ViewGroup has only two direct sub-Views, its behavior is more complex due to |
| * 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 content View will receive the touch event. Mind that content is actually underneath the |
| * caption, so we need to introduce our own dispatch ordering. We achieve this by overriding |
| * {@link #buildTouchDispatchChildList()}.</li> |
| * <li>If the touch event is not consumed by the content View, it will go to the caption View |
| * and the dragging logic will be executed.</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); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mCaption = getChildAt(0); |
| } |
| |
| public void setPhoneWindow(PhoneWindow owner, boolean show) { |
| mOwner = owner; |
| mShow = show; |
| mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled(); |
| if (mOverlayWithAppContent) { |
| // The caption is covering the content, so we make its background transparent to make |
| // the content visible. |
| mCaption.setBackgroundColor(Color.TRANSPARENT); |
| } |
| 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 ArrayList<View> buildTouchDispatchChildList() { |
| mTouchDispatchList.ensureCapacity(3); |
| if (mCaption != null) { |
| mTouchDispatchList.add(mCaption); |
| } |
| if (mContent != null) { |
| mTouchDispatchList.add(mContent); |
| } |
| return mTouchDispatchList; |
| } |
| |
| @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()); |
| } |
| } |
| |
| // 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); |
| } |
| } |
| } |