1 /* 2 * Copyright (C) 2014 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.systemui.shade; 18 19 import android.app.Fragment; 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.graphics.Canvas; 23 import android.graphics.Rect; 24 import android.util.AttributeSet; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.view.ViewGroup.MarginLayoutParams; 28 import android.view.WindowInsets; 29 30 import androidx.annotation.Nullable; 31 import androidx.constraintlayout.widget.ConstraintLayout; 32 import androidx.constraintlayout.widget.ConstraintSet; 33 34 import com.android.systemui.R; 35 import com.android.systemui.fragments.FragmentHostManager.FragmentListener; 36 import com.android.systemui.plugins.qs.QS; 37 import com.android.systemui.statusbar.notification.AboveShelfObserver; 38 39 import java.util.ArrayList; 40 import java.util.Comparator; 41 import java.util.function.Consumer; 42 43 /** 44 * The container with notification stack scroller and quick settings inside. 45 */ 46 public class NotificationsQuickSettingsContainer extends ConstraintLayout 47 implements FragmentListener, AboveShelfObserver.HasViewAboveShelfChangedListener { 48 49 private View mQsFrame; 50 private View mStackScroller; 51 private View mKeyguardStatusBar; 52 53 private final ArrayList<View> mDrawingOrderedChildren = new ArrayList<>(); 54 private final ArrayList<View> mLayoutDrawingOrder = new ArrayList<>(); 55 private final Comparator<View> mIndexComparator = Comparator.comparingInt(this::indexOfChild); 56 private Consumer<WindowInsets> mInsetsChangedListener = insets -> {}; 57 private Consumer<QS> mQSFragmentAttachedListener = qs -> {}; 58 private QS mQs; 59 private View mQSContainer; 60 private int mLastQSPaddingBottom; 61 private boolean mIsMigratingNSSL; 62 63 /** 64 * These are used to compute the bounding box containing the shade and the notification scrim, 65 * which is then used to drive the Back gesture animation. 66 */ 67 private final Rect mUpperRect = new Rect(); 68 private final Rect mBoundingBoxRect = new Rect(); 69 70 @Nullable 71 private Consumer<Configuration> mConfigurationChangedListener; 72 NotificationsQuickSettingsContainer(Context context, AttributeSet attrs)73 public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) { 74 super(context, attrs); 75 } 76 77 @Override onFinishInflate()78 protected void onFinishInflate() { 79 super.onFinishInflate(); 80 mQsFrame = findViewById(R.id.qs_frame); 81 mKeyguardStatusBar = findViewById(R.id.keyguard_header); 82 } 83 setStackScroller(View stackScroller)84 void setStackScroller(View stackScroller) { 85 mStackScroller = stackScroller; 86 } 87 88 @Override onFragmentViewCreated(String tag, Fragment fragment)89 public void onFragmentViewCreated(String tag, Fragment fragment) { 90 mQs = (QS) fragment; 91 mQSFragmentAttachedListener.accept(mQs); 92 mQSContainer = mQs.getView().findViewById(R.id.quick_settings_container); 93 // We need to restore the bottom padding as the fragment may have been recreated due to 94 // some special Configuration change, so we apply the last known padding (this will be 95 // correct even if it has changed while the fragment was destroyed and re-created). 96 setQSContainerPaddingBottom(mLastQSPaddingBottom); 97 } 98 99 @Override onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf)100 public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) { 101 invalidate(); 102 } 103 104 @Override onConfigurationChanged(Configuration newConfig)105 protected void onConfigurationChanged(Configuration newConfig) { 106 super.onConfigurationChanged(newConfig); 107 if (mConfigurationChangedListener != null) { 108 mConfigurationChangedListener.accept(newConfig); 109 } 110 } 111 setConfigurationChangedListener(Consumer<Configuration> listener)112 public void setConfigurationChangedListener(Consumer<Configuration> listener) { 113 mConfigurationChangedListener = listener; 114 } 115 setNotificationsMarginBottom(int margin)116 public void setNotificationsMarginBottom(int margin) { 117 MarginLayoutParams params = (MarginLayoutParams) mStackScroller.getLayoutParams(); 118 params.bottomMargin = margin; 119 mStackScroller.setLayoutParams(params); 120 } 121 setQSContainerPaddingBottom(int paddingBottom)122 public void setQSContainerPaddingBottom(int paddingBottom) { 123 mLastQSPaddingBottom = paddingBottom; 124 if (mQSContainer != null) { 125 mQSContainer.setPadding( 126 mQSContainer.getPaddingLeft(), 127 mQSContainer.getPaddingTop(), 128 mQSContainer.getPaddingRight(), 129 paddingBottom 130 ); 131 } 132 } 133 setInsetsChangedListener(Consumer<WindowInsets> onInsetsChangedListener)134 public void setInsetsChangedListener(Consumer<WindowInsets> onInsetsChangedListener) { 135 mInsetsChangedListener = onInsetsChangedListener; 136 } 137 removeOnInsetsChangedListener()138 public void removeOnInsetsChangedListener() { 139 mInsetsChangedListener = insets -> {}; 140 } 141 setQSFragmentAttachedListener(Consumer<QS> qsFragmentAttachedListener)142 public void setQSFragmentAttachedListener(Consumer<QS> qsFragmentAttachedListener) { 143 mQSFragmentAttachedListener = qsFragmentAttachedListener; 144 // listener might be attached after fragment is attached 145 if (mQs != null) { 146 mQSFragmentAttachedListener.accept(mQs); 147 } 148 } 149 removeQSFragmentAttachedListener()150 public void removeQSFragmentAttachedListener() { 151 mQSFragmentAttachedListener = qs -> {}; 152 } 153 154 @Override onApplyWindowInsets(WindowInsets insets)155 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 156 mInsetsChangedListener.accept(insets); 157 return insets; 158 } 159 160 @Override dispatchDraw(Canvas canvas)161 protected void dispatchDraw(Canvas canvas) { 162 mDrawingOrderedChildren.clear(); 163 mLayoutDrawingOrder.clear(); 164 if (mKeyguardStatusBar.getVisibility() == View.VISIBLE) { 165 mDrawingOrderedChildren.add(mKeyguardStatusBar); 166 mLayoutDrawingOrder.add(mKeyguardStatusBar); 167 } 168 if (mQsFrame.getVisibility() == View.VISIBLE) { 169 mDrawingOrderedChildren.add(mQsFrame); 170 mLayoutDrawingOrder.add(mQsFrame); 171 } 172 if (mStackScroller.getVisibility() == View.VISIBLE) { 173 mDrawingOrderedChildren.add(mStackScroller); 174 mLayoutDrawingOrder.add(mStackScroller); 175 } 176 177 // Let's now find the order that the view has when drawing regularly by sorting 178 mLayoutDrawingOrder.sort(mIndexComparator); 179 super.dispatchDraw(canvas); 180 } 181 setMigratingNSSL(boolean isMigrating)182 void setMigratingNSSL(boolean isMigrating) { 183 mIsMigratingNSSL = isMigrating; 184 } 185 186 @Override dispatchTouchEvent(MotionEvent ev)187 public boolean dispatchTouchEvent(MotionEvent ev) { 188 return TouchLogger.logDispatchTouch("NotificationsQuickSettingsContainer", ev, 189 super.dispatchTouchEvent(ev)); 190 } 191 192 @Override drawChild(Canvas canvas, View child, long drawingTime)193 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 194 if (mIsMigratingNSSL) { 195 return super.drawChild(canvas, child, drawingTime); 196 } 197 int layoutIndex = mLayoutDrawingOrder.indexOf(child); 198 if (layoutIndex >= 0) { 199 return super.drawChild(canvas, mDrawingOrderedChildren.get(layoutIndex), drawingTime); 200 } else { 201 return super.drawChild(canvas, child, drawingTime); 202 } 203 } 204 applyConstraints(ConstraintSet constraintSet)205 public void applyConstraints(ConstraintSet constraintSet) { 206 constraintSet.applyTo(this); 207 } 208 209 /** 210 * Scale multiple elements in tandem, for the predictive back animation. 211 * This is how the Shade responds to the Back gesture (by scaling). 212 * Without the common center, individual elements will scale about their respective centers. 213 * Scaling the entire NotificationsQuickSettingsContainer will also resize the shade header 214 * (which we don't want). 215 */ applyBackScaling(float scale, boolean usingSplitShade)216 public void applyBackScaling(float scale, boolean usingSplitShade) { 217 if (mStackScroller == null || mQSContainer == null) { 218 return; 219 } 220 221 mQSContainer.getBoundsOnScreen(mUpperRect); 222 mStackScroller.getBoundsOnScreen(mBoundingBoxRect); 223 mBoundingBoxRect.union(mUpperRect); 224 225 float cx = mBoundingBoxRect.centerX(); 226 float cy = mBoundingBoxRect.centerY(); 227 228 mQSContainer.setPivotX(cx); 229 mQSContainer.setPivotY(cy); 230 mQSContainer.setScaleX(scale); 231 mQSContainer.setScaleY(scale); 232 233 // When in large-screen split-shade mode, the notification stack scroller scales correctly 234 // only if the pivot point is at the left edge of the screen (because of its dimensions). 235 // When not in large-screen split-shade mode, we can scale correctly via the (cx,cy) above. 236 mStackScroller.setPivotX(usingSplitShade ? 0.0f : cx); 237 mStackScroller.setPivotY(cy); 238 mStackScroller.setScaleX(scale); 239 mStackScroller.setScaleY(scale); 240 } 241 } 242