Home | History | Annotate | Download | only in widget
      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 settle/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     // True when the left mouse button got released while dragging.
     86     private boolean mLeftMouseButtonReleased;
     87 
     88     private boolean mOverlayWithAppContent = false;
     89 
     90     private View mCaption;
     91     private View mContent;
     92     private View mMaximize;
     93     private View mClose;
     94 
     95     // Fields for detecting drag events.
     96     private int mTouchDownX;
     97     private int mTouchDownY;
     98     private boolean mCheckForDragging;
     99     private int mDragSlop;
    100 
    101     // Fields for detecting and intercepting click events on close/maximize.
    102     private ArrayList<View> mTouchDispatchList = new ArrayList<>(2);
    103     // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent
    104     // with existing click detection.
    105     private GestureDetector mGestureDetector;
    106     private final Rect mCloseRect = new Rect();
    107     private final Rect mMaximizeRect = new Rect();
    108     private View mClickTarget;
    109 
    110     public DecorCaptionView(Context context) {
    111         super(context);
    112         init(context);
    113     }
    114 
    115     public DecorCaptionView(Context context, AttributeSet attrs) {
    116         super(context, attrs);
    117         init(context);
    118     }
    119 
    120     public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
    121         super(context, attrs, defStyle);
    122         init(context);
    123     }
    124 
    125     private void init(Context context) {
    126         mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    127         mGestureDetector = new GestureDetector(context, this);
    128     }
    129 
    130     @Override
    131     protected void onFinishInflate() {
    132         super.onFinishInflate();
    133         mCaption = getChildAt(0);
    134     }
    135 
    136     public void setPhoneWindow(PhoneWindow owner, boolean show) {
    137         mOwner = owner;
    138         mShow = show;
    139         mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
    140         if (mOverlayWithAppContent) {
    141             // The caption is covering the content, so we make its background transparent to make
    142             // the content visible.
    143             mCaption.setBackgroundColor(Color.TRANSPARENT);
    144         }
    145         updateCaptionVisibility();
    146         // By changing the outline provider to BOUNDS, the window can remove its
    147         // background without removing the shadow.
    148         mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
    149         mMaximize = findViewById(R.id.maximize_window);
    150         mClose = findViewById(R.id.close_window);
    151     }
    152 
    153     @Override
    154     public boolean onInterceptTouchEvent(MotionEvent ev) {
    155         // If the user starts touch on the maximize/close buttons, we immediately intercept, so
    156         // that these buttons are always clickable.
    157         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    158             final int x = (int) ev.getX();
    159             final int y = (int) ev.getY();
    160             if (mMaximizeRect.contains(x, y)) {
    161                 mClickTarget = mMaximize;
    162             }
    163             if (mCloseRect.contains(x, y)) {
    164                 mClickTarget = mClose;
    165             }
    166         }
    167         return mClickTarget != null;
    168     }
    169 
    170     @Override
    171     public boolean onTouchEvent(MotionEvent event) {
    172         if (mClickTarget != null) {
    173             mGestureDetector.onTouchEvent(event);
    174             final int action = event.getAction();
    175             if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
    176                 mClickTarget = null;
    177             }
    178             return true;
    179         }
    180         return false;
    181     }
    182 
    183     @Override
    184     public boolean onTouch(View v, MotionEvent e) {
    185         // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
    186         // the old input device events get cancelled first. So no need to remember the kind of
    187         // input device we are listening to.
    188         final int x = (int) e.getX();
    189         final int y = (int) e.getY();
    190         switch (e.getActionMasked()) {
    191             case MotionEvent.ACTION_DOWN:
    192                 if (!mShow) {
    193                     // When there is no caption we should not react to anything.
    194                     return false;
    195                 }
    196                 // Checking for a drag action is started if we aren't dragging already and the
    197                 // starting event is either a left mouse button or any other input device.
    198                 if (((e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE ||
    199                         (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0))) {
    200                     mCheckForDragging = true;
    201                     mTouchDownX = x;
    202                     mTouchDownY = y;
    203                 }
    204                 break;
    205 
    206             case MotionEvent.ACTION_MOVE:
    207                 if (!mDragging && mCheckForDragging && passedSlop(x, y)) {
    208                     mCheckForDragging = false;
    209                     mDragging = true;
    210                     mLeftMouseButtonReleased = false;
    211                     startMovingTask(e.getRawX(), e.getRawY());
    212                 } else if (mDragging && !mLeftMouseButtonReleased) {
    213                     if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE &&
    214                             (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) {
    215                         // There is no separate mouse button up call and if the user mixes mouse
    216                         // button drag actions, we stop dragging once he releases the button.
    217                         mLeftMouseButtonReleased = true;
    218                         break;
    219                     }
    220                 }
    221                 break;
    222 
    223             case MotionEvent.ACTION_UP:
    224             case MotionEvent.ACTION_CANCEL:
    225                 if (!mDragging) {
    226                     break;
    227                 }
    228                 // Abort the ongoing dragging.
    229                 mDragging = false;
    230                 return !mCheckForDragging;
    231         }
    232         return mDragging || mCheckForDragging;
    233     }
    234 
    235     @Override
    236     public ArrayList<View> buildTouchDispatchChildList() {
    237         mTouchDispatchList.ensureCapacity(3);
    238         if (mCaption != null) {
    239             mTouchDispatchList.add(mCaption);
    240         }
    241         if (mContent != null) {
    242             mTouchDispatchList.add(mContent);
    243         }
    244         return mTouchDispatchList;
    245     }
    246 
    247     @Override
    248     public boolean shouldDelayChildPressedState() {
    249         return false;
    250     }
    251 
    252     private boolean passedSlop(int x, int y) {
    253         return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop;
    254     }
    255 
    256     /**
    257      * The phone window configuration has changed and the caption needs to be updated.
    258      * @param show True if the caption should be shown.
    259      */
    260     public void onConfigurationChanged(boolean show) {
    261         mShow = show;
    262         updateCaptionVisibility();
    263     }
    264 
    265     @Override
    266     public void addView(View child, int index, ViewGroup.LayoutParams params) {
    267         if (!(params instanceof MarginLayoutParams)) {
    268             throw new IllegalArgumentException(
    269                     "params " + params + " must subclass MarginLayoutParams");
    270         }
    271         // Make sure that we never get more then one client area in our view.
    272         if (index >= 2 || getChildCount() >= 2) {
    273             throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
    274         }
    275         // To support the overlaying content in the caption, we need to put the content view as the
    276         // first child to get the right Z-Ordering.
    277         super.addView(child, 0, params);
    278         mContent = child;
    279     }
    280 
    281     @Override
    282     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    283         final int captionHeight;
    284         if (mCaption.getVisibility() != View.GONE) {
    285             measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0);
    286             captionHeight = mCaption.getMeasuredHeight();
    287         } else {
    288             captionHeight = 0;
    289         }
    290         if (mContent != null) {
    291             if (mOverlayWithAppContent) {
    292                 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
    293             } else {
    294                 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec,
    295                         captionHeight);
    296             }
    297         }
    298 
    299         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
    300                 MeasureSpec.getSize(heightMeasureSpec));
    301     }
    302 
    303     @Override
    304     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    305         final int captionHeight;
    306         if (mCaption.getVisibility() != View.GONE) {
    307             mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
    308             captionHeight = mCaption.getBottom() - mCaption.getTop();
    309             mMaximize.getHitRect(mMaximizeRect);
    310             mClose.getHitRect(mCloseRect);
    311         } else {
    312             captionHeight = 0;
    313             mMaximizeRect.setEmpty();
    314             mCloseRect.setEmpty();
    315         }
    316 
    317         if (mContent != null) {
    318             if (mOverlayWithAppContent) {
    319                 mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
    320             } else {
    321                 mContent.layout(0, captionHeight, mContent.getMeasuredWidth(),
    322                         captionHeight + mContent.getMeasuredHeight());
    323             }
    324         }
    325 
    326         // This assumes that the caption bar is at the top.
    327         mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
    328                 mClose.getRight(), mClose.getBottom());
    329     }
    330     /**
    331      * Determine if the workspace is entirely covered by the window.
    332      * @return Returns true when the window is filling the entire screen/workspace.
    333      **/
    334     private boolean isFillingScreen() {
    335         return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
    336                 (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
    337                         View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
    338     }
    339 
    340     /**
    341      * Updates the visibility of the caption.
    342      **/
    343     private void updateCaptionVisibility() {
    344         // Don't show the caption if the window has e.g. entered full screen.
    345         boolean invisible = isFillingScreen() || !mShow;
    346         mCaption.setVisibility(invisible ? GONE : VISIBLE);
    347         mCaption.setOnTouchListener(this);
    348     }
    349 
    350     /**
    351      * Maximize the window by moving it to the maximized workspace stack.
    352      **/
    353     private void maximizeWindow() {
    354         Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
    355         if (callback != null) {
    356             try {
    357                 callback.exitFreeformMode();
    358             } catch (RemoteException ex) {
    359                 Log.e(TAG, "Cannot change task workspace.");
    360             }
    361         }
    362     }
    363 
    364     public boolean isCaptionShowing() {
    365         return mShow;
    366     }
    367 
    368     public int getCaptionHeight() {
    369         return (mCaption != null) ? mCaption.getHeight() : 0;
    370     }
    371 
    372     public void removeContentView() {
    373         if (mContent != null) {
    374             removeView(mContent);
    375             mContent = null;
    376         }
    377     }
    378 
    379     public View getCaption() {
    380         return mCaption;
    381     }
    382 
    383     @Override
    384     public LayoutParams generateLayoutParams(AttributeSet attrs) {
    385         return new MarginLayoutParams(getContext(), attrs);
    386     }
    387 
    388     @Override
    389     protected LayoutParams generateDefaultLayoutParams() {
    390         return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
    391                 MarginLayoutParams.MATCH_PARENT);
    392     }
    393 
    394     @Override
    395     protected LayoutParams generateLayoutParams(LayoutParams p) {
    396         return new MarginLayoutParams(p);
    397     }
    398 
    399     @Override
    400     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    401         return p instanceof MarginLayoutParams;
    402     }
    403 
    404     @Override
    405     public boolean onDown(MotionEvent e) {
    406         return false;
    407     }
    408 
    409     @Override
    410     public void onShowPress(MotionEvent e) {
    411 
    412     }
    413 
    414     @Override
    415     public boolean onSingleTapUp(MotionEvent e) {
    416         if (mClickTarget == mMaximize) {
    417             maximizeWindow();
    418         } else if (mClickTarget == mClose) {
    419             mOwner.dispatchOnWindowDismissed(true /*finishTask*/);
    420         }
    421         return true;
    422     }
    423 
    424     @Override
    425     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    426         return false;
    427     }
    428 
    429     @Override
    430     public void onLongPress(MotionEvent e) {
    431 
    432     }
    433 
    434     @Override
    435     public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    436         return false;
    437     }
    438 }
    439