Home | History | Annotate | Download | only in systemui
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
      5  * except in compliance with the License. You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the
     10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     11  * KIND, either express or implied. See the License for the specific language governing
     12  * permissions and limitations under the License.
     13  */
     14 
     15 package com.android.systemui;
     16 
     17 import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE;
     18 import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE;
     19 import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE;
     20 
     21 import android.animation.Animator;
     22 import android.animation.AnimatorListenerAdapter;
     23 import android.animation.AnimatorSet;
     24 import android.animation.ObjectAnimator;
     25 import android.content.Context;
     26 import android.provider.Settings;
     27 import android.util.AttributeSet;
     28 import android.view.Gravity;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 import android.view.ViewOutlineProvider;
     32 import android.view.ViewTreeObserver;
     33 import android.widget.LinearLayout;
     34 
     35 import com.android.systemui.tuner.TunerService;
     36 import com.android.systemui.tuner.TunerService.Tunable;
     37 import com.android.systemui.util.leak.RotationUtils;
     38 
     39 /**
     40  * Layout for placing two containers at a specific physical position on the device, relative to the
     41  * device's hardware, regardless of screen rotation.
     42  */
     43 public class HardwareUiLayout extends MultiListLayout implements Tunable {
     44 
     45     private static final String EDGE_BLEED = "sysui_hwui_edge_bleed";
     46     private static final String ROUNDED_DIVIDER = "sysui_hwui_rounded_divider";
     47     private final int[] mTmp2 = new int[2];
     48     private ViewGroup mList;
     49     private ViewGroup mSeparatedView;
     50     private int mOldHeight;
     51     private boolean mAnimating;
     52     private AnimatorSet mAnimation;
     53     private View mDivision;
     54     private HardwareBgDrawable mListBackground;
     55     private HardwareBgDrawable mSeparatedViewBackground;
     56     private Animator mAnimator;
     57     private boolean mCollapse;
     58     private int mEndPoint;
     59     private boolean mEdgeBleed;
     60     private boolean mRoundedDivider;
     61     private boolean mRotatedBackground;
     62     private boolean mSwapOrientation = true;
     63 
     64     public HardwareUiLayout(Context context, AttributeSet attrs) {
     65         super(context, attrs);
     66         // Manually re-initialize mRotation to portrait-mode, since this view must always
     67         // be constructed in portrait mode and rotated into the correct initial position.
     68         mRotation = ROTATION_NONE;
     69         updateSettings();
     70     }
     71 
     72     @Override
     73     protected ViewGroup getSeparatedView() {
     74         return findViewById(com.android.systemui.R.id.separated_button);
     75     }
     76 
     77     @Override
     78     protected ViewGroup getListView() {
     79         return findViewById(android.R.id.list);
     80     }
     81 
     82     @Override
     83     protected void onAttachedToWindow() {
     84         super.onAttachedToWindow();
     85         updateSettings();
     86         Dependency.get(TunerService.class).addTunable(this, EDGE_BLEED, ROUNDED_DIVIDER);
     87         getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
     88     }
     89 
     90     @Override
     91     protected void onDetachedFromWindow() {
     92         super.onDetachedFromWindow();
     93         getViewTreeObserver().removeOnComputeInternalInsetsListener(mInsetsListener);
     94         Dependency.get(TunerService.class).removeTunable(this);
     95     }
     96 
     97     @Override
     98     public void onTuningChanged(String key, String newValue) {
     99         updateSettings();
    100     }
    101 
    102     private void updateSettings() {
    103         mEdgeBleed = Settings.Secure.getInt(getContext().getContentResolver(),
    104                 EDGE_BLEED, 0) != 0;
    105         mRoundedDivider = Settings.Secure.getInt(getContext().getContentResolver(),
    106                 ROUNDED_DIVIDER, 0) != 0;
    107         updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding());
    108         mListBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed, getContext());
    109         mSeparatedViewBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed,
    110                 getContext());
    111         if (mList != null) {
    112             mList.setBackground(mListBackground);
    113             mSeparatedView.setBackground(mSeparatedViewBackground);
    114             requestLayout();
    115         }
    116     }
    117 
    118     private void updateEdgeMargin(int edge) {
    119         if (mList != null) {
    120             MarginLayoutParams params = (MarginLayoutParams) mList.getLayoutParams();
    121             if (mRotation == ROTATION_LANDSCAPE) {
    122                 params.topMargin = edge;
    123             } else if (mRotation == ROTATION_SEASCAPE) {
    124                 params.bottomMargin = edge;
    125             } else {
    126                 params.rightMargin = edge;
    127             }
    128             mList.setLayoutParams(params);
    129         }
    130 
    131         if (mSeparatedView != null) {
    132             MarginLayoutParams params = (MarginLayoutParams) mSeparatedView.getLayoutParams();
    133             if (mRotation == ROTATION_LANDSCAPE) {
    134                 params.topMargin = edge;
    135             } else if (mRotation == ROTATION_SEASCAPE) {
    136                 params.bottomMargin = edge;
    137             } else {
    138                 params.rightMargin = edge;
    139             }
    140             mSeparatedView.setLayoutParams(params);
    141         }
    142     }
    143 
    144     private int getEdgePadding() {
    145         return getContext().getResources().getDimensionPixelSize(R.dimen.edge_margin);
    146     }
    147 
    148     @Override
    149     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    150         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    151         if (mList == null) {
    152             if (getChildCount() != 0) {
    153                 mList = getListView();
    154                 mList.setBackground(mListBackground);
    155                 mSeparatedView = getSeparatedView();
    156                 mSeparatedView.setBackground(mSeparatedViewBackground);
    157                 updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding());
    158                 mOldHeight = mList.getMeasuredHeight();
    159 
    160                 // Must be called to initialize view rotation correctly.
    161                 // Requires LayoutParams, hence why this isn't called during the constructor.
    162                 updateRotation();
    163             } else {
    164                 return;
    165             }
    166         }
    167         int newHeight = mList.getMeasuredHeight();
    168         if (newHeight != mOldHeight) {
    169             animateChild(mOldHeight, newHeight);
    170         }
    171 
    172         post(() -> updatePaddingAndGravityIfTooTall());
    173         post(() -> updatePosition());
    174     }
    175 
    176     public void setSwapOrientation(boolean swapOrientation) {
    177         mSwapOrientation = swapOrientation;
    178     }
    179 
    180     private void updateRotation() {
    181         int rotation = RotationUtils.getRotation(getContext());
    182         if (rotation != mRotation) {
    183             rotate(mRotation, rotation);
    184             mRotation = rotation;
    185         }
    186     }
    187 
    188     /**
    189      *  Requires LayoutParams to be set to work correctly, and therefore must be run after after
    190      *  the HardwareUILayout has been added to the view hierarchy.
    191      */
    192     protected void rotate(int from, int to) {
    193         super.rotate(from, to);
    194         if (from != ROTATION_NONE && to != ROTATION_NONE) {
    195             // Rather than handling this confusing case, just do 2 rotations.
    196             rotate(from, ROTATION_NONE);
    197             rotate(ROTATION_NONE, to);
    198             return;
    199         }
    200         if (from == ROTATION_LANDSCAPE || to == ROTATION_SEASCAPE) {
    201             rotateRight();
    202         } else {
    203             rotateLeft();
    204         }
    205         if (mAdapter.hasSeparatedItems()) {
    206             if (from == ROTATION_SEASCAPE || to == ROTATION_SEASCAPE) {
    207                 // Separated view has top margin, so seascape separated view need special rotation,
    208                 // not a full left or right rotation.
    209                 swapLeftAndTop(mSeparatedView);
    210             } else if (from == ROTATION_LANDSCAPE) {
    211                 rotateRight(mSeparatedView);
    212             } else {
    213                 rotateLeft(mSeparatedView);
    214             }
    215         }
    216         if (to != ROTATION_NONE) {
    217             if (mList instanceof LinearLayout) {
    218                 mRotatedBackground = true;
    219                 mListBackground.setRotatedBackground(true);
    220                 mSeparatedViewBackground.setRotatedBackground(true);
    221                 LinearLayout linearLayout = (LinearLayout) mList;
    222                 if (mSwapOrientation) {
    223                     linearLayout.setOrientation(LinearLayout.HORIZONTAL);
    224                     setOrientation(LinearLayout.HORIZONTAL);
    225                 }
    226                 swapDimens(mList);
    227                 swapDimens(mSeparatedView);
    228             }
    229         } else {
    230             if (mList instanceof LinearLayout) {
    231                 mRotatedBackground = false;
    232                 mListBackground.setRotatedBackground(false);
    233                 mSeparatedViewBackground.setRotatedBackground(false);
    234                 LinearLayout linearLayout = (LinearLayout) mList;
    235                 if (mSwapOrientation) {
    236                     linearLayout.setOrientation(LinearLayout.VERTICAL);
    237                     setOrientation(LinearLayout.VERTICAL);
    238                 }
    239                 swapDimens(mList);
    240                 swapDimens(mSeparatedView);
    241             }
    242         }
    243     }
    244 
    245     @Override
    246     public void onUpdateList() {
    247         super.onUpdateList();
    248 
    249         for (int i = 0; i < mAdapter.getCount(); i++) {
    250             ViewGroup parent;
    251             boolean separated = mAdapter.shouldBeSeparated(i);
    252             if (separated) {
    253                 parent = getSeparatedView();
    254             } else {
    255                 parent = getListView();
    256             }
    257             View v = mAdapter.getView(i, null, parent);
    258             parent.addView(v);
    259         }
    260     }
    261 
    262     private void rotateRight() {
    263         rotateRight(this);
    264         rotateRight(mList);
    265         swapDimens(this);
    266 
    267         LayoutParams p = (LayoutParams) mList.getLayoutParams();
    268         p.gravity = rotateGravityRight(p.gravity);
    269         mList.setLayoutParams(p);
    270 
    271         LayoutParams separatedViewLayoutParams = (LayoutParams) mSeparatedView.getLayoutParams();
    272         separatedViewLayoutParams.gravity = rotateGravityRight(separatedViewLayoutParams.gravity);
    273         mSeparatedView.setLayoutParams(separatedViewLayoutParams);
    274 
    275         setGravity(rotateGravityRight(getGravity()));
    276     }
    277 
    278     private void swapDimens(View v) {
    279         ViewGroup.LayoutParams params = v.getLayoutParams();
    280         int h = params.width;
    281         params.width = params.height;
    282         params.height = h;
    283         v.setLayoutParams(params);
    284     }
    285 
    286     private int rotateGravityRight(int gravity) {
    287         int retGravity = 0;
    288         int layoutDirection = getLayoutDirection();
    289         final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
    290         final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
    291 
    292         switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
    293             case Gravity.CENTER_HORIZONTAL:
    294                 retGravity |= Gravity.CENTER_VERTICAL;
    295                 break;
    296             case Gravity.RIGHT:
    297                 retGravity |= Gravity.BOTTOM;
    298                 break;
    299             case Gravity.LEFT:
    300             default:
    301                 retGravity |= Gravity.TOP;
    302                 break;
    303         }
    304 
    305         switch (verticalGravity) {
    306             case Gravity.CENTER_VERTICAL:
    307                 retGravity |= Gravity.CENTER_HORIZONTAL;
    308                 break;
    309             case Gravity.BOTTOM:
    310                 retGravity |= Gravity.LEFT;
    311                 break;
    312             case Gravity.TOP:
    313             default:
    314                 retGravity |= Gravity.RIGHT;
    315                 break;
    316         }
    317         return retGravity;
    318     }
    319 
    320     private void rotateLeft() {
    321         rotateLeft(this);
    322         rotateLeft(mList);
    323         swapDimens(this);
    324 
    325         LayoutParams p = (LayoutParams) mList.getLayoutParams();
    326         p.gravity = rotateGravityLeft(p.gravity);
    327         mList.setLayoutParams(p);
    328 
    329         LayoutParams separatedViewLayoutParams = (LayoutParams) mSeparatedView.getLayoutParams();
    330         separatedViewLayoutParams.gravity = rotateGravityLeft(separatedViewLayoutParams.gravity);
    331         mSeparatedView.setLayoutParams(separatedViewLayoutParams);
    332 
    333         setGravity(rotateGravityLeft(getGravity()));
    334     }
    335 
    336     private int rotateGravityLeft(int gravity) {
    337         if (gravity == -1) {
    338             gravity = Gravity.TOP | Gravity.START;
    339         }
    340         int retGravity = 0;
    341         int layoutDirection = getLayoutDirection();
    342         final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
    343         final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
    344 
    345         switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
    346             case Gravity.CENTER_HORIZONTAL:
    347                 retGravity |= Gravity.CENTER_VERTICAL;
    348                 break;
    349             case Gravity.RIGHT:
    350                 retGravity |= Gravity.TOP;
    351                 break;
    352             case Gravity.LEFT:
    353             default:
    354                 retGravity |= Gravity.BOTTOM;
    355                 break;
    356         }
    357 
    358         switch (verticalGravity) {
    359             case Gravity.CENTER_VERTICAL:
    360                 retGravity |= Gravity.CENTER_HORIZONTAL;
    361                 break;
    362             case Gravity.BOTTOM:
    363                 retGravity |= Gravity.RIGHT;
    364                 break;
    365             case Gravity.TOP:
    366             default:
    367                 retGravity |= Gravity.LEFT;
    368                 break;
    369         }
    370         return retGravity;
    371     }
    372 
    373     private void rotateLeft(View v) {
    374         v.setPadding(v.getPaddingTop(), v.getPaddingRight(), v.getPaddingBottom(),
    375                 v.getPaddingLeft());
    376         MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
    377         params.setMargins(params.topMargin, params.rightMargin, params.bottomMargin,
    378                 params.leftMargin);
    379         v.setLayoutParams(params);
    380     }
    381 
    382     private void rotateRight(View v) {
    383         v.setPadding(v.getPaddingBottom(), v.getPaddingLeft(), v.getPaddingTop(),
    384                 v.getPaddingRight());
    385         MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
    386         params.setMargins(params.bottomMargin, params.leftMargin, params.topMargin,
    387                 params.rightMargin);
    388         v.setLayoutParams(params);
    389     }
    390 
    391     private void swapLeftAndTop(View v) {
    392         v.setPadding(v.getPaddingTop(), v.getPaddingLeft(), v.getPaddingBottom(),
    393                 v.getPaddingRight());
    394         MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
    395         params.setMargins(params.topMargin, params.leftMargin, params.bottomMargin,
    396                 params.rightMargin);
    397         v.setLayoutParams(params);
    398     }
    399 
    400     @Override
    401     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    402         super.onLayout(changed, left, top, right, bottom);
    403 
    404         post(() -> updatePosition());
    405 
    406     }
    407 
    408     private void animateChild(int oldHeight, int newHeight) {
    409         if (true) return;
    410         if (mAnimating) {
    411             mAnimation.cancel();
    412         }
    413         mAnimating = true;
    414         mAnimation = new AnimatorSet();
    415         mAnimation.addListener(new AnimatorListenerAdapter() {
    416             @Override
    417             public void onAnimationEnd(Animator animation) {
    418                 mAnimating = false;
    419             }
    420         });
    421         int fromTop = mList.getTop();
    422         int fromBottom = mList.getBottom();
    423         int toTop = fromTop - ((newHeight - oldHeight) / 2);
    424         int toBottom = fromBottom + ((newHeight - oldHeight) / 2);
    425         ObjectAnimator top = ObjectAnimator.ofInt(mList, "top", fromTop, toTop);
    426         top.addUpdateListener(animation -> mListBackground.invalidateSelf());
    427         mAnimation.playTogether(top,
    428                 ObjectAnimator.ofInt(mList, "bottom", fromBottom, toBottom));
    429     }
    430 
    431     public void setDivisionView(View v) {
    432         mDivision = v;
    433         if (mDivision != null) {
    434             mDivision.addOnLayoutChangeListener(
    435                     (v1, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
    436                             updatePosition());
    437         }
    438         updatePosition();
    439     }
    440 
    441     private void updatePosition() {
    442         if (mList == null) return;
    443         // If got separated button, setRotatedBackground to false,
    444         // all items won't get white background.
    445         boolean separated = mAdapter.hasSeparatedItems();
    446         mListBackground.setRotatedBackground(separated);
    447         mSeparatedViewBackground.setRotatedBackground(separated);
    448         if (mDivision != null && mDivision.getVisibility() == VISIBLE) {
    449             int index = mRotatedBackground ? 0 : 1;
    450             mDivision.getLocationOnScreen(mTmp2);
    451             float trans = mRotatedBackground ? mDivision.getTranslationX()
    452                     : mDivision.getTranslationY();
    453             int viewTop = (int) (mTmp2[index] + trans);
    454             mList.getLocationOnScreen(mTmp2);
    455             viewTop -= mTmp2[index];
    456             setCutPoint(viewTop);
    457         } else {
    458             setCutPoint(mList.getMeasuredHeight());
    459         }
    460     }
    461 
    462     private void setCutPoint(int point) {
    463         int curPoint = mListBackground.getCutPoint();
    464         if (curPoint == point) return;
    465         if (getAlpha() == 0 || curPoint == 0) {
    466             mListBackground.setCutPoint(point);
    467             return;
    468         }
    469         if (mAnimator != null) {
    470             if (mEndPoint == point) {
    471                 return;
    472             }
    473             mAnimator.cancel();
    474         }
    475         mEndPoint = point;
    476         mAnimator = ObjectAnimator.ofInt(mListBackground, "cutPoint", curPoint, point);
    477         if (mCollapse) {
    478             mAnimator.setStartDelay(300);
    479             mCollapse = false;
    480         }
    481         mAnimator.start();
    482     }
    483 
    484     // If current power menu height larger then screen height, remove padding to break power menu
    485     // alignment and set menu center vertical within the screen.
    486     private void updatePaddingAndGravityIfTooTall() {
    487         int defaultTopPadding;
    488         int viewsTotalHeight;
    489         int separatedViewTopMargin;
    490         int screenHeight;
    491         int totalHeight;
    492         int targetGravity;
    493         boolean separated = mAdapter.hasSeparatedItems();
    494         MarginLayoutParams params = (MarginLayoutParams) mSeparatedView.getLayoutParams();
    495         switch (RotationUtils.getRotation(getContext())) {
    496             case RotationUtils.ROTATION_LANDSCAPE:
    497                 defaultTopPadding = getPaddingLeft();
    498                 viewsTotalHeight = mList.getMeasuredWidth() + mSeparatedView.getMeasuredWidth();
    499                 separatedViewTopMargin = separated ? params.leftMargin : 0;
    500                 screenHeight = getMeasuredWidth();
    501                 targetGravity = Gravity.CENTER_HORIZONTAL|Gravity.TOP;
    502                 break;
    503             case RotationUtils.ROTATION_SEASCAPE:
    504                 defaultTopPadding = getPaddingRight();
    505                 viewsTotalHeight = mList.getMeasuredWidth() + mSeparatedView.getMeasuredWidth();
    506                 separatedViewTopMargin = separated ? params.leftMargin : 0;
    507                 screenHeight = getMeasuredWidth();
    508                 targetGravity = Gravity.CENTER_HORIZONTAL|Gravity.BOTTOM;
    509                 break;
    510             default: // Portrait
    511                 defaultTopPadding = getPaddingTop();
    512                 viewsTotalHeight = mList.getMeasuredHeight() + mSeparatedView.getMeasuredHeight();
    513                 separatedViewTopMargin = separated ? params.topMargin : 0;
    514                 screenHeight = getMeasuredHeight();
    515                 targetGravity = Gravity.CENTER_VERTICAL|Gravity.RIGHT;
    516                 break;
    517         }
    518         totalHeight = defaultTopPadding + viewsTotalHeight + separatedViewTopMargin;
    519         if (totalHeight >= screenHeight) {
    520             setPadding(0, 0, 0, 0);
    521             setGravity(targetGravity);
    522         }
    523     }
    524 
    525     @Override
    526     public ViewOutlineProvider getOutlineProvider() {
    527         return super.getOutlineProvider();
    528     }
    529 
    530     public void setCollapse() {
    531         mCollapse = true;
    532     }
    533 
    534     private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = inoutInfo -> {
    535         if (mHasOutsideTouch || (mList == null)) {
    536             inoutInfo.setTouchableInsets(
    537                     ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
    538             return;
    539         }
    540         inoutInfo.setTouchableInsets(
    541                 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT);
    542         inoutInfo.contentInsets.set(mList.getLeft(), mList.getTop(),
    543                 0, getBottom() - mList.getBottom());
    544     };
    545 
    546     private float getAnimationDistance() {
    547         return getContext().getResources().getDimension(
    548                 com.android.systemui.R.dimen.global_actions_panel_width) / 2;
    549     }
    550 
    551     @Override
    552     public float getAnimationOffsetX() {
    553         if (RotationUtils.getRotation(mContext) == ROTATION_NONE) {
    554             return getAnimationDistance();
    555         }
    556         return 0;
    557     }
    558 
    559     @Override
    560     public float getAnimationOffsetY() {
    561         switch (RotationUtils.getRotation(getContext())) {
    562             case RotationUtils.ROTATION_LANDSCAPE:
    563                 return -getAnimationDistance();
    564             case RotationUtils.ROTATION_SEASCAPE:
    565                 return getAnimationDistance();
    566             default: // Portrait
    567                 return 0;
    568         }
    569     }
    570 }