• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 android.view;
18 
19 import static android.app.Flags.notificationsRedesignTemplates;
20 import static android.util.MathUtils.abs;
21 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
22 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.compat.annotation.UnsupportedAppUsage;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.graphics.Canvas;
30 import android.graphics.Outline;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Drawable;
33 import android.os.Build;
34 import android.util.AttributeSet;
35 import android.widget.FrameLayout;
36 import android.widget.RelativeLayout;
37 import android.widget.RemoteViews;
38 import android.widget.TextView;
39 
40 import com.android.internal.R;
41 import com.android.internal.widget.CachingIconView;
42 import com.android.internal.widget.NotificationExpandButton;
43 
44 import java.util.ArrayList;
45 
46 /**
47  * A header of a notification view
48  *
49  * @hide
50  */
51 @RemoteViews.RemoteView
52 public class NotificationHeaderView extends RelativeLayout {
53     private final int mTouchableHeight;
54     private OnClickListener mExpandClickListener;
55     private HeaderTouchListener mTouchListener = new HeaderTouchListener();
56     private NotificationTopLineView mTopLineView;
57     private NotificationExpandButton mExpandButton;
58     private View mAltExpandTarget;
59     private CachingIconView mIcon;
60     private Drawable mBackground;
61     private boolean mEntireHeaderClickable;
62     private boolean mExpandOnlyOnButton;
63     private boolean mAcceptAllTouches;
64     private float mTopLineTranslation;
65     private float mExpandButtonTranslation;
66 
67     ViewOutlineProvider mProvider = new ViewOutlineProvider() {
68         @Override
69         public void getOutline(View view, Outline outline) {
70             if (mBackground != null) {
71                 outline.setRect(0, 0, getWidth(), getHeight());
72                 outline.setAlpha(1f);
73             }
74         }
75     };
76 
NotificationHeaderView(Context context)77     public NotificationHeaderView(Context context) {
78         this(context, null);
79     }
80 
81     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
NotificationHeaderView(Context context, @Nullable AttributeSet attrs)82     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) {
83         this(context, attrs, 0);
84     }
85 
NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)86     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
87         this(context, attrs, defStyleAttr, 0);
88     }
89 
NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)90     public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr,
91             int defStyleRes) {
92         super(context, attrs, defStyleAttr, defStyleRes);
93         Resources res = getResources();
94         mTouchableHeight = res.getDimensionPixelSize(R.dimen.notification_header_touchable_height);
95         mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand);
96     }
97 
98     @Override
onFinishInflate()99     protected void onFinishInflate() {
100         super.onFinishInflate();
101         mIcon = findViewById(R.id.icon);
102         mTopLineView = findViewById(R.id.notification_top_line);
103         mExpandButton = findViewById(R.id.expand_button);
104         mAltExpandTarget = findViewById(R.id.alternate_expand_target);
105         setClipToPadding(false);
106     }
107 
108     /**
109      * Set a {@link Drawable} to be displayed as a background on the header.
110      */
setHeaderBackgroundDrawable(Drawable drawable)111     public void setHeaderBackgroundDrawable(Drawable drawable) {
112         if (drawable != null) {
113             setWillNotDraw(false);
114             mBackground = drawable;
115             mBackground.setCallback(this);
116             setOutlineProvider(mProvider);
117         } else {
118             setWillNotDraw(true);
119             mBackground = null;
120             setOutlineProvider(null);
121         }
122         invalidate();
123     }
124 
125     @Override
onDraw(Canvas canvas)126     protected void onDraw(Canvas canvas) {
127         if (mBackground != null) {
128             mBackground.setBounds(0, 0, getWidth(), getHeight());
129             mBackground.draw(canvas);
130         }
131     }
132 
133     @Override
verifyDrawable(@onNull Drawable who)134     protected boolean verifyDrawable(@NonNull Drawable who) {
135         return super.verifyDrawable(who) || who == mBackground;
136     }
137 
138     @Override
drawableStateChanged()139     protected void drawableStateChanged() {
140         if (mBackground != null && mBackground.isStateful()) {
141             mBackground.setState(getDrawableState());
142         }
143     }
144 
updateTouchListener()145     private void updateTouchListener() {
146         if (mExpandClickListener == null) {
147             setOnTouchListener(null);
148             return;
149         }
150         setOnTouchListener(mTouchListener);
151         mTouchListener.bindTouchRects();
152     }
153 
154     @Override
setOnClickListener(@ullable OnClickListener l)155     public void setOnClickListener(@Nullable OnClickListener l) {
156         mExpandClickListener = l;
157         mExpandButton.setOnClickListener(mExpandClickListener);
158         mAltExpandTarget.setOnClickListener(mExpandClickListener);
159         updateTouchListener();
160     }
161 
162     /**
163      * Sets the extra margin at the end of the top line of left-aligned text + icons.
164      * This value will have the margin required to accommodate the expand button added to it.
165      *
166      * @param extraMarginEnd extra margin in px
167      */
setTopLineExtraMarginEnd(int extraMarginEnd)168     public void setTopLineExtraMarginEnd(int extraMarginEnd) {
169         mTopLineView.setHeaderTextMarginEnd(extraMarginEnd);
170     }
171 
172     /**
173      * Sets the extra margin at the end of the top line of left-aligned text + icons.
174      * This value will have the margin required to accommodate the expand button added to it.
175      *
176      * @param extraMarginEndDp extra margin in dp
177      */
178     @RemotableViewMethod
setTopLineExtraMarginEndDp(float extraMarginEndDp)179     public void setTopLineExtraMarginEndDp(float extraMarginEndDp) {
180         setTopLineExtraMarginEnd(
181                 (int) (extraMarginEndDp * getResources().getDisplayMetrics().density));
182     }
183 
184     /**
185      * Center top line  and expand button vertically.
186      */
187     @RemotableViewMethod
centerTopLine(boolean center)188     public void centerTopLine(boolean center) {
189         if (notificationsRedesignTemplates()) {
190             // The content of the top line view is already center-aligned, but since the height
191             // matches the content by default, it looks top-aligned. If the height matches the
192             // parent instead, the text ends up correctly centered in the parent.
193             ViewGroup.LayoutParams lp = mTopLineView.getLayoutParams();
194             lp.height = center ? MATCH_PARENT : WRAP_CONTENT;
195             mTopLineView.setLayoutParams(lp);
196 
197             centerExpandButton(center);
198         }
199     }
200 
201     /** Center expand button vertically. */
centerExpandButton(boolean center)202     private void centerExpandButton(boolean center) {
203         ViewGroup.LayoutParams lp = mExpandButton.getLayoutParams();
204         lp.height = center ? MATCH_PARENT : WRAP_CONTENT;
205         if (lp instanceof FrameLayout.LayoutParams flp) {
206             flp.gravity = center ? Gravity.CENTER : (Gravity.TOP | Gravity.END);
207         }
208         mExpandButton.setLayoutParams(lp);
209     }
210 
211     /** The view containing the app name, timestamp etc at the top of the notification. */
getTopLineView()212     public NotificationTopLineView getTopLineView() {
213         return mTopLineView;
214     }
215 
216     /** The view containing the button to expand the notification. */
getExpandButton()217     public NotificationExpandButton getExpandButton() {
218         return mExpandButton;
219     }
220 
221     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)222     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
223         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
224 
225         if (notificationsRedesignTemplates()) {
226             // TODO: b/378660052 - These should never be null in practice, consider using
227             //  requireViewById() in the onFinishInflate.
228             if (mTopLineView != null) {
229                 mTopLineTranslation = measureCenterTranslation(mTopLineView);
230             }
231             if (mExpandButton != null) {
232                 mExpandButtonTranslation = measureCenterTranslation(mExpandButton);
233             }
234         }
235     }
236 
measureCenterTranslation(View view)237     private float measureCenterTranslation(View view) {
238         // When the view is centered (see centerTopLine), its height is MATCH_PARENT
239         int parentHeight = getMeasuredHeight();
240         // When the view is top-aligned, its height is WRAP_CONTENT
241         float wrapContentHeight = view.getMeasuredHeight();
242         // Calculate the translation needed between the two alignments
243         final MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
244         return abs((parentHeight - wrapContentHeight) / 2f - lp.topMargin);
245     }
246 
247     /**
248      * The vertical translation necessary between the two positions of the top line, to be used in
249      * the animation. See also {@link NotificationHeaderView#centerTopLine(boolean)}.
250      */
getTopLineTranslation()251     public float getTopLineTranslation() {
252         return mTopLineTranslation;
253     }
254 
255     /**
256      * The vertical translation necessary between the two positions of the expander, to be used in
257      * the animation. See also {@link NotificationHeaderView#centerTopLine(boolean)}.
258      */
getExpandButtonTranslation()259     public float getExpandButtonTranslation() {
260         return mExpandButtonTranslation;
261     }
262 
263     /**
264      * This is used to make the low-priority header show the bolded text of a title.
265      *
266      * @param styleTextAsTitle true if this header's text is to have the style of a title
267      */
268     @RemotableViewMethod
styleTextAsTitle(boolean styleTextAsTitle)269     public void styleTextAsTitle(boolean styleTextAsTitle) {
270         int styleResId = styleTextAsTitle
271                 ? R.style.TextAppearance_DeviceDefault_Notification_Title
272                 : R.style.TextAppearance_DeviceDefault_Notification_Info;
273         // Most of the time, we're showing text in the minimized state
274         View headerText = findViewById(R.id.header_text);
275         if (headerText instanceof TextView) {
276             ((TextView) headerText).setTextAppearance(styleResId);
277         }
278         // If there's no summary or text, we show the app name instead of nothing
279         View appNameText = findViewById(R.id.app_name_text);
280         if (appNameText instanceof TextView) {
281             ((TextView) appNameText).setTextAppearance(styleResId);
282         }
283     }
284 
285     /**
286      * Handles clicks on the header based on the region tapped.
287      */
288     public class HeaderTouchListener implements OnTouchListener {
289 
290         private final ArrayList<Rect> mTouchRects = new ArrayList<>();
291         private Rect mExpandButtonRect;
292         private Rect mAltExpandTargetRect;
293         private int mTouchSlop;
294         private boolean mTrackGesture;
295         private float mDownX;
296         private float mDownY;
297 
HeaderTouchListener()298         public HeaderTouchListener() {
299         }
300 
bindTouchRects()301         public void bindTouchRects() {
302             mTouchRects.clear();
303             addRectAroundView(mIcon);
304             mExpandButtonRect = addRectAroundView(mExpandButton);
305             mAltExpandTargetRect = addRectAroundView(mAltExpandTarget);
306             addWidthRect();
307             mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
308         }
309 
addWidthRect()310         private void addWidthRect() {
311             Rect r = new Rect();
312             r.top = 0;
313             r.bottom = mTouchableHeight;
314             r.left = 0;
315             r.right = getWidth();
316             mTouchRects.add(r);
317         }
318 
addRectAroundView(View view)319         private Rect addRectAroundView(View view) {
320             final Rect r = getRectAroundView(view);
321             mTouchRects.add(r);
322             return r;
323         }
324 
getRectAroundView(View view)325         private Rect getRectAroundView(View view) {
326             float size = 48 * getResources().getDisplayMetrics().density;
327             float width = Math.max(size, view.getWidth());
328             float height = Math.max(size, view.getHeight());
329             final Rect r = new Rect();
330             if (view.getVisibility() == GONE) {
331                 view = getFirstChildNotGone();
332                 r.left = (int) (view.getLeft() - width / 2.0f);
333             } else {
334                 r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f);
335             }
336             r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f);
337             r.bottom = (int) (r.top + height);
338             r.right = (int) (r.left + width);
339             return r;
340         }
341 
342         @Override
onTouch(View v, MotionEvent event)343         public boolean onTouch(View v, MotionEvent event) {
344             float x = event.getX();
345             float y = event.getY();
346             switch (event.getActionMasked() & MotionEvent.ACTION_MASK) {
347                 case MotionEvent.ACTION_DOWN:
348                     mTrackGesture = false;
349                     if (isInside(x, y)) {
350                         mDownX = x;
351                         mDownY = y;
352                         mTrackGesture = true;
353                         return true;
354                     }
355                     break;
356                 case MotionEvent.ACTION_MOVE:
357                     if (mTrackGesture) {
358                         if (Math.abs(mDownX - x) > mTouchSlop
359                                 || Math.abs(mDownY - y) > mTouchSlop) {
360                             mTrackGesture = false;
361                         }
362                     }
363                     break;
364                 case MotionEvent.ACTION_UP:
365                     if (mTrackGesture) {
366                         float topLineX = mTopLineView.getX();
367                         float topLineY = mTopLineView.getY();
368                         if (mTopLineView.onTouchUp(x - topLineX, y - topLineY,
369                                 mDownX - topLineX, mDownY - topLineY)) {
370                             break;
371                         }
372                         mExpandButton.performClick();
373                     }
374                     break;
375             }
376             return mTrackGesture;
377         }
378 
isInside(float x, float y)379         private boolean isInside(float x, float y) {
380             if (mAcceptAllTouches) {
381                 return true;
382             }
383             if (mExpandOnlyOnButton) {
384                 return mExpandButtonRect.contains((int) x, (int) y)
385                         || mAltExpandTargetRect.contains((int) x, (int) y);
386             }
387             for (int i = 0; i < mTouchRects.size(); i++) {
388                 Rect r = mTouchRects.get(i);
389                 if (r.contains((int) x, (int) y)) {
390                     return true;
391                 }
392             }
393             float topLineX = x - mTopLineView.getX();
394             float topLineY = y - mTopLineView.getY();
395             return mTopLineView.isInTouchRect(topLineX, topLineY);
396         }
397     }
398 
getFirstChildNotGone()399     private View getFirstChildNotGone() {
400         for (int i = 0; i < getChildCount(); i++) {
401             final View child = getChildAt(i);
402             if (child.getVisibility() != GONE) {
403                 return child;
404             }
405         }
406         return this;
407     }
408 
409     @Override
hasOverlappingRendering()410     public boolean hasOverlappingRendering() {
411         return false;
412     }
413 
isInTouchRect(float x, float y)414     public boolean isInTouchRect(float x, float y) {
415         if (mExpandClickListener == null) {
416             return false;
417         }
418         return mTouchListener.isInside(x, y);
419     }
420 
421     /**
422      * Sets whether or not all touches to this header view will register as a click. Note that
423      * if the config value for {@code config_notificationHeaderClickableForExpand} is {@code true},
424      * then calling this method with {@code false} will not override that configuration.
425      */
426     @RemotableViewMethod
setAcceptAllTouches(boolean acceptAllTouches)427     public void setAcceptAllTouches(boolean acceptAllTouches) {
428         mAcceptAllTouches = mEntireHeaderClickable || acceptAllTouches;
429     }
430 
431     /**
432      * Sets whether only the expand icon itself should serve as the expand target.
433      */
434     @RemotableViewMethod
setExpandOnlyOnButton(boolean expandOnlyOnButton)435     public void setExpandOnlyOnButton(boolean expandOnlyOnButton) {
436         mExpandOnlyOnButton = expandOnlyOnButton;
437     }
438 }
439