huangcm
2025-04-11 48566d1cda2d109a94496c806286f47b8984166d
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
/*
 * Copyright (C) 2019 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.bubbles;
 
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
 
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
 
import com.android.systemui.R;
 
/** Dismiss view that contains a scrim gradient, as well as a dismiss icon, text, and circle. */
public class BubbleDismissView extends FrameLayout {
    /** Duration for animations involving the dismiss target text/icon/gradient. */
    private static final int DISMISS_TARGET_ANIMATION_BASE_DURATION = 150;
 
    private View mDismissGradient;
 
    private LinearLayout mDismissTarget;
    private ImageView mDismissIcon;
    private TextView mDismissText;
    private View mDismissCircle;
 
    private SpringAnimation mDismissTargetAlphaSpring;
    private SpringAnimation mDismissTargetVerticalSpring;
 
    public BubbleDismissView(Context context) {
        super(context);
        setVisibility(GONE);
 
        mDismissGradient = new FrameLayout(mContext);
 
        FrameLayout.LayoutParams gradientParams =
                new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
        gradientParams.gravity = Gravity.BOTTOM;
        mDismissGradient.setLayoutParams(gradientParams);
 
        Drawable gradient = mContext.getResources().getDrawable(R.drawable.pip_dismiss_scrim);
        gradient.setAlpha((int) (255 * 0.85f));
        mDismissGradient.setBackground(gradient);
 
        mDismissGradient.setVisibility(GONE);
        addView(mDismissGradient);
 
        LayoutInflater.from(context).inflate(R.layout.bubble_dismiss_target, this, true);
        mDismissTarget = findViewById(R.id.bubble_dismiss_icon_container);
        mDismissIcon = findViewById(R.id.bubble_dismiss_close_icon);
        mDismissText = findViewById(R.id.bubble_dismiss_text);
        mDismissCircle = findViewById(R.id.bubble_dismiss_circle);
 
        // Set up the basic target area animations. These are very simple animations that don't need
        // fancy interpolators.
        final AccelerateDecelerateInterpolator interpolator =
                new AccelerateDecelerateInterpolator();
        mDismissGradient.animate()
                .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION)
                .setInterpolator(interpolator);
        mDismissText.animate()
                .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION)
                .setInterpolator(interpolator);
        mDismissIcon.animate()
                .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION)
                .setInterpolator(interpolator);
        mDismissCircle.animate()
                .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION / 2)
                .setInterpolator(interpolator);
 
        mDismissTargetAlphaSpring =
                new SpringAnimation(mDismissTarget, DynamicAnimation.ALPHA)
                        .setSpring(new SpringForce()
                                .setStiffness(SpringForce.STIFFNESS_LOW)
                                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
        mDismissTargetVerticalSpring =
                new SpringAnimation(mDismissTarget, DynamicAnimation.TRANSLATION_Y)
                        .setSpring(new SpringForce()
                                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
                                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
 
        mDismissTargetAlphaSpring.addEndListener((anim, canceled, alpha, velocity) -> {
            // Since DynamicAnimations end when they're 'nearly' done, we can't rely on alpha being
            // exactly zero when this listener is triggered. However, if it's less than 50% we can
            // safely assume it was animating out rather than in.
            if (alpha < 0.5f) {
                // If the alpha spring was animating the view out, set it to GONE when it's done.
                setVisibility(GONE);
            }
        });
    }
 
    /** Springs in the dismiss target and fades in the gradient. */
    void springIn() {
        setVisibility(View.VISIBLE);
 
        // Fade in the dismiss target (icon + text).
        mDismissTarget.setAlpha(0f);
        mDismissTargetAlphaSpring.animateToFinalPosition(1f);
 
        // Spring up the dismiss target (icon + text).
        mDismissTarget.setTranslationY(mDismissTarget.getHeight() / 2f);
        mDismissTargetVerticalSpring.animateToFinalPosition(0);
 
        // Fade in the gradient.
        mDismissGradient.setVisibility(VISIBLE);
        mDismissGradient.animate().alpha(1f);
 
        // Make sure the dismiss elements are in the separated position (in case we hid the target
        // while they were condensed to cover the bubbles being in the target).
        mDismissIcon.setAlpha(1f);
        mDismissIcon.setScaleX(1f);
        mDismissIcon.setScaleY(1f);
        mDismissIcon.setTranslationX(0f);
        mDismissText.setAlpha(1f);
        mDismissText.setTranslationX(0f);
    }
 
    /** Springs out the dismiss target and fades out the gradient. */
    void springOut() {
        // Fade out the target.
        mDismissTargetAlphaSpring.animateToFinalPosition(0f);
 
        // Spring the target down a bit.
        mDismissTargetVerticalSpring.animateToFinalPosition(mDismissTarget.getHeight() / 2f);
 
        // Fade out the gradient and then set it to GONE so it's not in the SBV hierarchy.
        mDismissGradient.animate().alpha(0f).withEndAction(
                () -> mDismissGradient.setVisibility(GONE));
 
        // Pop out the dismiss circle.
        mDismissCircle.animate().alpha(0f).scaleX(1.2f).scaleY(1.2f);
    }
 
    /**
     * Encircles the center of the dismiss target, pulling the X towards the center and hiding the
     * text.
     */
    void animateEncircleCenterWithX(boolean encircle) {
        // Pull the text towards the center if we're encircling (it'll be faded out, leaving only
        // the X icon over the bubbles), or back to normal if we're un-encircling.
        final float textTranslation = encircle
                ? -mDismissIcon.getWidth() / 4f
                : 0f;
 
        // Center the icon if we're encircling, or put it back to normal if not.
        final float iconTranslation = encircle
                ? mDismissTarget.getWidth() / 2f
                - mDismissIcon.getWidth() / 2f
                - mDismissIcon.getLeft()
                : 0f;
 
        // Fade in/out the text and translate it.
        mDismissText.animate()
                .alpha(encircle ? 0f : 1f)
                .translationX(textTranslation);
 
        mDismissIcon.animate()
                .setDuration(150)
                .translationX(iconTranslation);
 
        // Fade out the gradient if we're encircling (the bubbles will 'absorb' it by darkening
        // themselves).
        mDismissGradient.animate()
                .alpha(encircle ? 0f : 1f);
 
        // Prepare the circle to be 'dropped in'.
        if (encircle) {
            mDismissCircle.setAlpha(0f);
            mDismissCircle.setScaleX(1.2f);
            mDismissCircle.setScaleY(1.2f);
        }
 
        // Drop in the circle, or pull it back up.
        mDismissCircle.animate()
                .alpha(encircle ? 1f : 0f)
                .scaleX(encircle ? 1f : 0f)
                .scaleY(encircle ? 1f : 0f);
    }
 
    /** Animates the circle and the centered icon out. */
    void animateEncirclingCircleDisappearance() {
        // Pop out the dismiss icon and circle.
        mDismissIcon.animate()
                .setDuration(50)
                .scaleX(0.9f)
                .scaleY(0.9f)
                .alpha(0f);
        mDismissCircle.animate()
                .scaleX(0.9f)
                .scaleY(0.9f)
                .alpha(0f);
    }
 
    /** Returns the Y value of the center of the dismiss target. */
    float getDismissTargetCenterY() {
        return getTop() + mDismissTarget.getTop() + mDismissTarget.getHeight() / 2f;
    }
 
    /** Returns the dismiss target, which contains the text/icon and any added padding. */
    View getDismissTarget() {
        return mDismissTarget;
    }
}