huangcm
2025-08-14 5d6606c55520a76d5bb8297d83fd9bbf967e5244
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
/*
 * Copyright (C) 2018 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.qs;
 
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewParent;
import android.widget.LinearLayout;
 
import androidx.core.widget.NestedScrollView;
 
import com.android.systemui.R;
import com.android.systemui.qs.touch.OverScroll;
import com.android.systemui.qs.touch.SwipeDetector;
 
/**
 * Quick setting scroll view containing the brightness slider and the QS tiles.
 *
 * <p>Call {@link #shouldIntercept(MotionEvent)} from parent views'
 * {@link #onInterceptTouchEvent(MotionEvent)} method to determine whether this view should
 * consume the touch event.
 */
public class QSScrollLayout extends NestedScrollView {
    private final int mTouchSlop;
    private final int mFooterHeight;
    private int mLastMotionY;
    private final SwipeDetector mSwipeDetector;
    private final OverScrollHelper mOverScrollHelper;
    private float mContentTranslationY;
 
    public QSScrollLayout(Context context, View... children) {
        super(context);
        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        mFooterHeight = getResources().getDimensionPixelSize(R.dimen.qs_footer_height);
        LinearLayout linearLayout = new LinearLayout(mContext);
        linearLayout.setLayoutParams(new LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT,
            LinearLayout.LayoutParams.WRAP_CONTENT));
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        for (View view : children) {
            linearLayout.addView(view);
        }
        addView(linearLayout);
        setOverScrollMode(OVER_SCROLL_NEVER);
        mOverScrollHelper = new OverScrollHelper();
        mSwipeDetector = new SwipeDetector(context, mOverScrollHelper, SwipeDetector.VERTICAL);
        mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, true);
    }
 
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!canScrollVertically(1) && !canScrollVertically(-1)) {
            return false;
        }
        mSwipeDetector.onTouchEvent(ev);
        return super.onInterceptTouchEvent(ev) || mOverScrollHelper.isInOverScroll();
    }
 
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!canScrollVertically(1) && !canScrollVertically(-1)) {
            return false;
        }
        mSwipeDetector.onTouchEvent(ev);
        return super.onTouchEvent(ev);
    }
 
    @Override
    protected void dispatchDraw(Canvas canvas) {
        canvas.translate(0, mContentTranslationY);
        super.dispatchDraw(canvas);
        canvas.translate(0, -mContentTranslationY);
    }
 
    public boolean shouldIntercept(MotionEvent ev) {
        if (ev.getY() > (getBottom() - mFooterHeight)) {
            // Do not intercept touches that are below the divider between QS and the footer.
            return false;
        }
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            mLastMotionY = (int) ev.getY();
        } else if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
            // Do not allow NotificationPanelView to intercept touch events when this
            // view can be scrolled down.
            if (mLastMotionY >= 0 && Math.abs(ev.getY() - mLastMotionY) > mTouchSlop
                    && canScrollVertically(1)) {
                requestParentDisallowInterceptTouchEvent(true);
                mLastMotionY = (int) ev.getY();
                return true;
            }
        } else if (ev.getActionMasked() == MotionEvent.ACTION_CANCEL
            || ev.getActionMasked() == MotionEvent.ACTION_UP) {
            mLastMotionY = -1;
            requestParentDisallowInterceptTouchEvent(false);
        }
        return false;
    }
 
    private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
        final ViewParent parent = getParent();
        if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }
 
    private void setContentTranslationY(float contentTranslationY) {
        mContentTranslationY = contentTranslationY;
        invalidate();
    }
 
    private static final Property<QSScrollLayout, Float> CONTENT_TRANS_Y =
            new Property<QSScrollLayout, Float>(Float.class, "qsScrollLayoutContentTransY") {
                @Override
                public Float get(QSScrollLayout qsScrollLayout) {
                    return qsScrollLayout.mContentTranslationY;
                }
 
                @Override
                public void set(QSScrollLayout qsScrollLayout, Float y) {
                    qsScrollLayout.setContentTranslationY(y);
                }
            };
 
    private class OverScrollHelper implements SwipeDetector.Listener {
        private boolean mIsInOverScroll;
 
        // We use this value to calculate the actual amount the user has overscrolled.
        private float mFirstDisplacement = 0;
 
        @Override
        public void onDragStart(boolean start) {}
 
        @Override
        public boolean onDrag(float displacement, float velocity) {
            // Only overscroll if the user is scrolling down when they're already at the bottom
            // or scrolling up when they're already at the top.
            boolean wasInOverScroll = mIsInOverScroll;
            mIsInOverScroll = (!canScrollVertically(1) && displacement < 0) ||
                    (!canScrollVertically(-1) && displacement > 0);
 
            if (wasInOverScroll && !mIsInOverScroll) {
                // Exit overscroll. This can happen when the user is in overscroll and then
                // scrolls the opposite way. Note that this causes the reset translation animation
                // to run while the user is dragging, which feels a bit unnatural.
                reset();
            } else if (mIsInOverScroll) {
                if (Float.compare(mFirstDisplacement, 0) == 0) {
                    // Because users can scroll before entering overscroll, we need to
                    // subtract the amount where the user was not in overscroll.
                    mFirstDisplacement = displacement;
                }
                float overscrollY = displacement - mFirstDisplacement;
                setContentTranslationY(getDampedOverScroll(overscrollY));
            }
 
            return mIsInOverScroll;
        }
 
        @Override
        public void onDragEnd(float velocity, boolean fling) {
            reset();
        }
 
        private void reset() {
            if (Float.compare(mContentTranslationY, 0) != 0) {
                ObjectAnimator.ofFloat(QSScrollLayout.this, CONTENT_TRANS_Y, 0)
                        .setDuration(100)
                        .start();
            }
            mIsInOverScroll = false;
            mFirstDisplacement = 0;
        }
 
        public boolean isInOverScroll() {
            return mIsInOverScroll;
        }
 
        private float getDampedOverScroll(float y) {
            return OverScroll.dampedScroll(y, getHeight());
        }
    }
}