blob: ad2e0024065f76b8211238f93a5a400a37f61e37 [file] [log] [blame]
/*
* Copyright (C) 2017 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.systemui;
import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE;
import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE;
import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.provider.Settings;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;
import com.android.systemui.util.leak.RotationUtils;
/**
* Layout for placing two containers at a specific physical position on the device, relative to the
* device's hardware, regardless of screen rotation.
*/
public class HardwareUiLayout extends MultiListLayout implements Tunable {
private static final String EDGE_BLEED = "sysui_hwui_edge_bleed";
private static final String ROUNDED_DIVIDER = "sysui_hwui_rounded_divider";
private final int[] mTmp2 = new int[2];
private ViewGroup mList;
private ViewGroup mSeparatedView;
private int mOldHeight;
private boolean mAnimating;
private AnimatorSet mAnimation;
private View mDivision;
private HardwareBgDrawable mListBackground;
private HardwareBgDrawable mSeparatedViewBackground;
private Animator mAnimator;
private boolean mCollapse;
private int mEndPoint;
private boolean mEdgeBleed;
private boolean mRoundedDivider;
private boolean mRotatedBackground;
private boolean mSwapOrientation = true;
public HardwareUiLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// Manually re-initialize mRotation to portrait-mode, since this view must always
// be constructed in portrait mode and rotated into the correct initial position.
mRotation = ROTATION_NONE;
updateSettings();
}
@Override
protected ViewGroup getSeparatedView() {
return findViewById(com.android.systemui.R.id.separated_button);
}
@Override
protected ViewGroup getListView() {
return findViewById(android.R.id.list);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
updateSettings();
Dependency.get(TunerService.class).addTunable(this, EDGE_BLEED, ROUNDED_DIVIDER);
getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnComputeInternalInsetsListener(mInsetsListener);
Dependency.get(TunerService.class).removeTunable(this);
}
@Override
public void onTuningChanged(String key, String newValue) {
updateSettings();
}
private void updateSettings() {
mEdgeBleed = Settings.Secure.getInt(getContext().getContentResolver(),
EDGE_BLEED, 0) != 0;
mRoundedDivider = Settings.Secure.getInt(getContext().getContentResolver(),
ROUNDED_DIVIDER, 0) != 0;
updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding());
mListBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed, getContext());
mSeparatedViewBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed,
getContext());
if (mList != null) {
mList.setBackground(mListBackground);
mSeparatedView.setBackground(mSeparatedViewBackground);
requestLayout();
}
}
private void updateEdgeMargin(int edge) {
if (mList != null) {
MarginLayoutParams params = (MarginLayoutParams) mList.getLayoutParams();
if (mRotation == ROTATION_LANDSCAPE) {
params.topMargin = edge;
} else if (mRotation == ROTATION_SEASCAPE) {
params.bottomMargin = edge;
} else {
params.rightMargin = edge;
}
mList.setLayoutParams(params);
}
if (mSeparatedView != null) {
MarginLayoutParams params = (MarginLayoutParams) mSeparatedView.getLayoutParams();
if (mRotation == ROTATION_LANDSCAPE) {
params.topMargin = edge;
} else if (mRotation == ROTATION_SEASCAPE) {
params.bottomMargin = edge;
} else {
params.rightMargin = edge;
}
mSeparatedView.setLayoutParams(params);
}
}
private int getEdgePadding() {
return getContext().getResources().getDimensionPixelSize(R.dimen.edge_margin);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mList == null) {
if (getChildCount() != 0) {
mList = getListView();
mList.setBackground(mListBackground);
mSeparatedView = getSeparatedView();
mSeparatedView.setBackground(mSeparatedViewBackground);
updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding());
mOldHeight = mList.getMeasuredHeight();
// Must be called to initialize view rotation correctly.
// Requires LayoutParams, hence why this isn't called during the constructor.
updateRotation();
} else {
return;
}
}
int newHeight = mList.getMeasuredHeight();
if (newHeight != mOldHeight) {
animateChild(mOldHeight, newHeight);
}
post(() -> updatePaddingAndGravityIfTooTall());
post(() -> updatePosition());
}
public void setSwapOrientation(boolean swapOrientation) {
mSwapOrientation = swapOrientation;
}
private void updateRotation() {
int rotation = RotationUtils.getRotation(getContext());
if (rotation != mRotation) {
rotate(mRotation, rotation);
mRotation = rotation;
}
}
/**
* Requires LayoutParams to be set to work correctly, and therefore must be run after after
* the HardwareUILayout has been added to the view hierarchy.
*/
protected void rotate(int from, int to) {
super.rotate(from, to);
if (from != ROTATION_NONE && to != ROTATION_NONE) {
// Rather than handling this confusing case, just do 2 rotations.
rotate(from, ROTATION_NONE);
rotate(ROTATION_NONE, to);
return;
}
if (from == ROTATION_LANDSCAPE || to == ROTATION_SEASCAPE) {
rotateRight();
} else {
rotateLeft();
}
if (mAdapter.hasSeparatedItems()) {
if (from == ROTATION_SEASCAPE || to == ROTATION_SEASCAPE) {
// Separated view has top margin, so seascape separated view need special rotation,
// not a full left or right rotation.
swapLeftAndTop(mSeparatedView);
} else if (from == ROTATION_LANDSCAPE) {
rotateRight(mSeparatedView);
} else {
rotateLeft(mSeparatedView);
}
}
if (to != ROTATION_NONE) {
if (mList instanceof LinearLayout) {
mRotatedBackground = true;
mListBackground.setRotatedBackground(true);
mSeparatedViewBackground.setRotatedBackground(true);
LinearLayout linearLayout = (LinearLayout) mList;
if (mSwapOrientation) {
linearLayout.setOrientation(LinearLayout.HORIZONTAL);
setOrientation(LinearLayout.HORIZONTAL);
}
swapDimens(mList);
swapDimens(mSeparatedView);
}
} else {
if (mList instanceof LinearLayout) {
mRotatedBackground = false;
mListBackground.setRotatedBackground(false);
mSeparatedViewBackground.setRotatedBackground(false);
LinearLayout linearLayout = (LinearLayout) mList;
if (mSwapOrientation) {
linearLayout.setOrientation(LinearLayout.VERTICAL);
setOrientation(LinearLayout.VERTICAL);
}
swapDimens(mList);
swapDimens(mSeparatedView);
}
}
}
@Override
public void onUpdateList() {
super.onUpdateList();
for (int i = 0; i < mAdapter.getCount(); i++) {
ViewGroup parent;
boolean separated = mAdapter.shouldBeSeparated(i);
if (separated) {
parent = getSeparatedView();
} else {
parent = getListView();
}
View v = mAdapter.getView(i, null, parent);
parent.addView(v);
}
}
private void rotateRight() {
rotateRight(this);
rotateRight(mList);
swapDimens(this);
LayoutParams p = (LayoutParams) mList.getLayoutParams();
p.gravity = rotateGravityRight(p.gravity);
mList.setLayoutParams(p);
LayoutParams separatedViewLayoutParams = (LayoutParams) mSeparatedView.getLayoutParams();
separatedViewLayoutParams.gravity = rotateGravityRight(separatedViewLayoutParams.gravity);
mSeparatedView.setLayoutParams(separatedViewLayoutParams);
setGravity(rotateGravityRight(getGravity()));
}
private void swapDimens(View v) {
ViewGroup.LayoutParams params = v.getLayoutParams();
int h = params.width;
params.width = params.height;
params.height = h;
v.setLayoutParams(params);
}
private int rotateGravityRight(int gravity) {
int retGravity = 0;
int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
retGravity |= Gravity.CENTER_VERTICAL;
break;
case Gravity.RIGHT:
retGravity |= Gravity.BOTTOM;
break;
case Gravity.LEFT:
default:
retGravity |= Gravity.TOP;
break;
}
switch (verticalGravity) {
case Gravity.CENTER_VERTICAL:
retGravity |= Gravity.CENTER_HORIZONTAL;
break;
case Gravity.BOTTOM:
retGravity |= Gravity.LEFT;
break;
case Gravity.TOP:
default:
retGravity |= Gravity.RIGHT;
break;
}
return retGravity;
}
private void rotateLeft() {
rotateLeft(this);
rotateLeft(mList);
swapDimens(this);
LayoutParams p = (LayoutParams) mList.getLayoutParams();
p.gravity = rotateGravityLeft(p.gravity);
mList.setLayoutParams(p);
LayoutParams separatedViewLayoutParams = (LayoutParams) mSeparatedView.getLayoutParams();
separatedViewLayoutParams.gravity = rotateGravityLeft(separatedViewLayoutParams.gravity);
mSeparatedView.setLayoutParams(separatedViewLayoutParams);
setGravity(rotateGravityLeft(getGravity()));
}
private int rotateGravityLeft(int gravity) {
if (gravity == -1) {
gravity = Gravity.TOP | Gravity.START;
}
int retGravity = 0;
int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
retGravity |= Gravity.CENTER_VERTICAL;
break;
case Gravity.RIGHT:
retGravity |= Gravity.TOP;
break;
case Gravity.LEFT:
default:
retGravity |= Gravity.BOTTOM;
break;
}
switch (verticalGravity) {
case Gravity.CENTER_VERTICAL:
retGravity |= Gravity.CENTER_HORIZONTAL;
break;
case Gravity.BOTTOM:
retGravity |= Gravity.RIGHT;
break;
case Gravity.TOP:
default:
retGravity |= Gravity.LEFT;
break;
}
return retGravity;
}
private void rotateLeft(View v) {
v.setPadding(v.getPaddingTop(), v.getPaddingRight(), v.getPaddingBottom(),
v.getPaddingLeft());
MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
params.setMargins(params.topMargin, params.rightMargin, params.bottomMargin,
params.leftMargin);
v.setLayoutParams(params);
}
private void rotateRight(View v) {
v.setPadding(v.getPaddingBottom(), v.getPaddingLeft(), v.getPaddingTop(),
v.getPaddingRight());
MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
params.setMargins(params.bottomMargin, params.leftMargin, params.topMargin,
params.rightMargin);
v.setLayoutParams(params);
}
private void swapLeftAndTop(View v) {
v.setPadding(v.getPaddingTop(), v.getPaddingLeft(), v.getPaddingBottom(),
v.getPaddingRight());
MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
params.setMargins(params.topMargin, params.leftMargin, params.bottomMargin,
params.rightMargin);
v.setLayoutParams(params);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
post(() -> updatePosition());
}
private void animateChild(int oldHeight, int newHeight) {
if (true) return;
if (mAnimating) {
mAnimation.cancel();
}
mAnimating = true;
mAnimation = new AnimatorSet();
mAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mAnimating = false;
}
});
int fromTop = mList.getTop();
int fromBottom = mList.getBottom();
int toTop = fromTop - ((newHeight - oldHeight) / 2);
int toBottom = fromBottom + ((newHeight - oldHeight) / 2);
ObjectAnimator top = ObjectAnimator.ofInt(mList, "top", fromTop, toTop);
top.addUpdateListener(animation -> mListBackground.invalidateSelf());
mAnimation.playTogether(top,
ObjectAnimator.ofInt(mList, "bottom", fromBottom, toBottom));
}
public void setDivisionView(View v) {
mDivision = v;
if (mDivision != null) {
mDivision.addOnLayoutChangeListener(
(v1, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
updatePosition());
}
updatePosition();
}
private void updatePosition() {
if (mList == null) return;
// If got separated button, setRotatedBackground to false,
// all items won't get white background.
boolean separated = mAdapter.hasSeparatedItems();
mListBackground.setRotatedBackground(separated);
mSeparatedViewBackground.setRotatedBackground(separated);
if (mDivision != null && mDivision.getVisibility() == VISIBLE) {
int index = mRotatedBackground ? 0 : 1;
mDivision.getLocationOnScreen(mTmp2);
float trans = mRotatedBackground ? mDivision.getTranslationX()
: mDivision.getTranslationY();
int viewTop = (int) (mTmp2[index] + trans);
mList.getLocationOnScreen(mTmp2);
viewTop -= mTmp2[index];
setCutPoint(viewTop);
} else {
setCutPoint(mList.getMeasuredHeight());
}
}
private void setCutPoint(int point) {
int curPoint = mListBackground.getCutPoint();
if (curPoint == point) return;
if (getAlpha() == 0 || curPoint == 0) {
mListBackground.setCutPoint(point);
return;
}
if (mAnimator != null) {
if (mEndPoint == point) {
return;
}
mAnimator.cancel();
}
mEndPoint = point;
mAnimator = ObjectAnimator.ofInt(mListBackground, "cutPoint", curPoint, point);
if (mCollapse) {
mAnimator.setStartDelay(300);
mCollapse = false;
}
mAnimator.start();
}
// If current power menu height larger then screen height, remove padding to break power menu
// alignment and set menu center vertical within the screen.
private void updatePaddingAndGravityIfTooTall() {
int defaultTopPadding;
int viewsTotalHeight;
int separatedViewTopMargin;
int screenHeight;
int totalHeight;
int targetGravity;
boolean separated = mAdapter.hasSeparatedItems();
MarginLayoutParams params = (MarginLayoutParams) mSeparatedView.getLayoutParams();
switch (RotationUtils.getRotation(getContext())) {
case RotationUtils.ROTATION_LANDSCAPE:
defaultTopPadding = getPaddingLeft();
viewsTotalHeight = mList.getMeasuredWidth() + mSeparatedView.getMeasuredWidth();
separatedViewTopMargin = separated ? params.leftMargin : 0;
screenHeight = getMeasuredWidth();
targetGravity = Gravity.CENTER_HORIZONTAL|Gravity.TOP;
break;
case RotationUtils.ROTATION_SEASCAPE:
defaultTopPadding = getPaddingRight();
viewsTotalHeight = mList.getMeasuredWidth() + mSeparatedView.getMeasuredWidth();
separatedViewTopMargin = separated ? params.leftMargin : 0;
screenHeight = getMeasuredWidth();
targetGravity = Gravity.CENTER_HORIZONTAL|Gravity.BOTTOM;
break;
default: // Portrait
defaultTopPadding = getPaddingTop();
viewsTotalHeight = mList.getMeasuredHeight() + mSeparatedView.getMeasuredHeight();
separatedViewTopMargin = separated ? params.topMargin : 0;
screenHeight = getMeasuredHeight();
targetGravity = Gravity.CENTER_VERTICAL|Gravity.RIGHT;
break;
}
totalHeight = defaultTopPadding + viewsTotalHeight + separatedViewTopMargin;
if (totalHeight >= screenHeight) {
setPadding(0, 0, 0, 0);
setGravity(targetGravity);
}
}
@Override
public ViewOutlineProvider getOutlineProvider() {
return super.getOutlineProvider();
}
public void setCollapse() {
mCollapse = true;
}
private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = inoutInfo -> {
if (mHasOutsideTouch || (mList == null)) {
inoutInfo.setTouchableInsets(
ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
return;
}
inoutInfo.setTouchableInsets(
ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT);
inoutInfo.contentInsets.set(mList.getLeft(), mList.getTop(),
0, getBottom() - mList.getBottom());
};
private float getAnimationDistance() {
return getContext().getResources().getDimension(
com.android.systemui.R.dimen.global_actions_panel_width) / 2;
}
@Override
public float getAnimationOffsetX() {
if (RotationUtils.getRotation(mContext) == ROTATION_NONE) {
return getAnimationDistance();
}
return 0;
}
@Override
public float getAnimationOffsetY() {
switch (RotationUtils.getRotation(getContext())) {
case RotationUtils.ROTATION_LANDSCAPE:
return -getAnimationDistance();
case RotationUtils.ROTATION_SEASCAPE:
return getAnimationDistance();
default: // Portrait
return 0;
}
}
}