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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.graphics.Canvas; 25 import android.graphics.Outline; 26 import android.graphics.Rect; 27 import android.graphics.drawable.Drawable; 28 import android.os.Build; 29 import android.util.AttributeSet; 30 import android.widget.RelativeLayout; 31 import android.widget.RemoteViews; 32 import android.widget.TextView; 33 34 import com.android.internal.R; 35 import com.android.internal.widget.CachingIconView; 36 import com.android.internal.widget.NotificationExpandButton; 37 38 import java.util.ArrayList; 39 40 /** 41 * A header of a notification view 42 * 43 * @hide 44 */ 45 @RemoteViews.RemoteView 46 public class NotificationHeaderView extends RelativeLayout { 47 private final int mTouchableHeight; 48 private OnClickListener mExpandClickListener; 49 private HeaderTouchListener mTouchListener = new HeaderTouchListener(); 50 private NotificationTopLineView mTopLineView; 51 private NotificationExpandButton mExpandButton; 52 private View mAltExpandTarget; 53 private CachingIconView mIcon; 54 private Drawable mBackground; 55 private boolean mEntireHeaderClickable; 56 private boolean mExpandOnlyOnButton; 57 private boolean mAcceptAllTouches; 58 59 ViewOutlineProvider mProvider = new ViewOutlineProvider() { 60 @Override 61 public void getOutline(View view, Outline outline) { 62 if (mBackground != null) { 63 outline.setRect(0, 0, getWidth(), getHeight()); 64 outline.setAlpha(1f); 65 } 66 } 67 }; 68 NotificationHeaderView(Context context)69 public NotificationHeaderView(Context context) { 70 this(context, null); 71 } 72 73 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) NotificationHeaderView(Context context, @Nullable AttributeSet attrs)74 public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) { 75 this(context, attrs, 0); 76 } 77 NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)78 public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 79 this(context, attrs, defStyleAttr, 0); 80 } 81 NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)82 public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, 83 int defStyleRes) { 84 super(context, attrs, defStyleAttr, defStyleRes); 85 Resources res = getResources(); 86 mTouchableHeight = res.getDimensionPixelSize(R.dimen.notification_header_touchable_height); 87 mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand); 88 } 89 90 @Override onFinishInflate()91 protected void onFinishInflate() { 92 super.onFinishInflate(); 93 mIcon = findViewById(R.id.icon); 94 mTopLineView = findViewById(R.id.notification_top_line); 95 mExpandButton = findViewById(R.id.expand_button); 96 mAltExpandTarget = findViewById(R.id.alternate_expand_target); 97 setClipToPadding(false); 98 } 99 100 /** 101 * Set a {@link Drawable} to be displayed as a background on the header. 102 */ setHeaderBackgroundDrawable(Drawable drawable)103 public void setHeaderBackgroundDrawable(Drawable drawable) { 104 if (drawable != null) { 105 setWillNotDraw(false); 106 mBackground = drawable; 107 mBackground.setCallback(this); 108 setOutlineProvider(mProvider); 109 } else { 110 setWillNotDraw(true); 111 mBackground = null; 112 setOutlineProvider(null); 113 } 114 invalidate(); 115 } 116 117 @Override onDraw(Canvas canvas)118 protected void onDraw(Canvas canvas) { 119 if (mBackground != null) { 120 mBackground.setBounds(0, 0, getWidth(), getHeight()); 121 mBackground.draw(canvas); 122 } 123 } 124 125 @Override verifyDrawable(@onNull Drawable who)126 protected boolean verifyDrawable(@NonNull Drawable who) { 127 return super.verifyDrawable(who) || who == mBackground; 128 } 129 130 @Override drawableStateChanged()131 protected void drawableStateChanged() { 132 if (mBackground != null && mBackground.isStateful()) { 133 mBackground.setState(getDrawableState()); 134 } 135 } 136 updateTouchListener()137 private void updateTouchListener() { 138 if (mExpandClickListener == null) { 139 setOnTouchListener(null); 140 return; 141 } 142 setOnTouchListener(mTouchListener); 143 mTouchListener.bindTouchRects(); 144 } 145 146 @Override setOnClickListener(@ullable OnClickListener l)147 public void setOnClickListener(@Nullable OnClickListener l) { 148 mExpandClickListener = l; 149 mExpandButton.setOnClickListener(mExpandClickListener); 150 mAltExpandTarget.setOnClickListener(mExpandClickListener); 151 updateTouchListener(); 152 } 153 154 /** 155 * Sets the extra margin at the end of the top line of left-aligned text + icons. 156 * This value will have the margin required to accommodate the expand button added to it. 157 * 158 * @param extraMarginEnd extra margin in px 159 */ setTopLineExtraMarginEnd(int extraMarginEnd)160 public void setTopLineExtraMarginEnd(int extraMarginEnd) { 161 mTopLineView.setHeaderTextMarginEnd(extraMarginEnd); 162 } 163 164 /** 165 * Sets the extra margin at the end of the top line of left-aligned text + icons. 166 * This value will have the margin required to accommodate the expand button added to it. 167 * 168 * @param extraMarginEndDp extra margin in dp 169 */ 170 @RemotableViewMethod setTopLineExtraMarginEndDp(float extraMarginEndDp)171 public void setTopLineExtraMarginEndDp(float extraMarginEndDp) { 172 setTopLineExtraMarginEnd( 173 (int) (extraMarginEndDp * getResources().getDisplayMetrics().density)); 174 } 175 176 /** 177 * This is used to make the low-priority header show the bolded text of a title. 178 * 179 * @param styleTextAsTitle true if this header's text is to have the style of a title 180 */ 181 @RemotableViewMethod styleTextAsTitle(boolean styleTextAsTitle)182 public void styleTextAsTitle(boolean styleTextAsTitle) { 183 int styleResId = styleTextAsTitle 184 ? R.style.TextAppearance_DeviceDefault_Notification_Title 185 : R.style.TextAppearance_DeviceDefault_Notification_Info; 186 // Most of the time, we're showing text in the minimized state 187 View headerText = findViewById(R.id.header_text); 188 if (headerText instanceof TextView) { 189 ((TextView) headerText).setTextAppearance(styleResId); 190 } 191 // If there's no summary or text, we show the app name instead of nothing 192 View appNameText = findViewById(R.id.app_name_text); 193 if (appNameText instanceof TextView) { 194 ((TextView) appNameText).setTextAppearance(styleResId); 195 } 196 } 197 198 /** 199 * Handles clicks on the header based on the region tapped. 200 */ 201 public class HeaderTouchListener implements OnTouchListener { 202 203 private final ArrayList<Rect> mTouchRects = new ArrayList<>(); 204 private Rect mExpandButtonRect; 205 private Rect mAltExpandTargetRect; 206 private int mTouchSlop; 207 private boolean mTrackGesture; 208 private float mDownX; 209 private float mDownY; 210 HeaderTouchListener()211 public HeaderTouchListener() { 212 } 213 bindTouchRects()214 public void bindTouchRects() { 215 mTouchRects.clear(); 216 addRectAroundView(mIcon); 217 mExpandButtonRect = addRectAroundView(mExpandButton); 218 mAltExpandTargetRect = addRectAroundView(mAltExpandTarget); 219 addWidthRect(); 220 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 221 } 222 addWidthRect()223 private void addWidthRect() { 224 Rect r = new Rect(); 225 r.top = 0; 226 r.bottom = mTouchableHeight; 227 r.left = 0; 228 r.right = getWidth(); 229 mTouchRects.add(r); 230 } 231 addRectAroundView(View view)232 private Rect addRectAroundView(View view) { 233 final Rect r = getRectAroundView(view); 234 mTouchRects.add(r); 235 return r; 236 } 237 getRectAroundView(View view)238 private Rect getRectAroundView(View view) { 239 float size = 48 * getResources().getDisplayMetrics().density; 240 float width = Math.max(size, view.getWidth()); 241 float height = Math.max(size, view.getHeight()); 242 final Rect r = new Rect(); 243 if (view.getVisibility() == GONE) { 244 view = getFirstChildNotGone(); 245 r.left = (int) (view.getLeft() - width / 2.0f); 246 } else { 247 r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f); 248 } 249 r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f); 250 r.bottom = (int) (r.top + height); 251 r.right = (int) (r.left + width); 252 return r; 253 } 254 255 @Override onTouch(View v, MotionEvent event)256 public boolean onTouch(View v, MotionEvent event) { 257 float x = event.getX(); 258 float y = event.getY(); 259 switch (event.getActionMasked() & MotionEvent.ACTION_MASK) { 260 case MotionEvent.ACTION_DOWN: 261 mTrackGesture = false; 262 if (isInside(x, y)) { 263 mDownX = x; 264 mDownY = y; 265 mTrackGesture = true; 266 return true; 267 } 268 break; 269 case MotionEvent.ACTION_MOVE: 270 if (mTrackGesture) { 271 if (Math.abs(mDownX - x) > mTouchSlop 272 || Math.abs(mDownY - y) > mTouchSlop) { 273 mTrackGesture = false; 274 } 275 } 276 break; 277 case MotionEvent.ACTION_UP: 278 if (mTrackGesture) { 279 float topLineX = mTopLineView.getX(); 280 float topLineY = mTopLineView.getY(); 281 if (mTopLineView.onTouchUp(x - topLineX, y - topLineY, 282 mDownX - topLineX, mDownY - topLineY)) { 283 break; 284 } 285 mExpandButton.performClick(); 286 } 287 break; 288 } 289 return mTrackGesture; 290 } 291 isInside(float x, float y)292 private boolean isInside(float x, float y) { 293 if (mAcceptAllTouches) { 294 return true; 295 } 296 if (mExpandOnlyOnButton) { 297 return mExpandButtonRect.contains((int) x, (int) y) 298 || mAltExpandTargetRect.contains((int) x, (int) y); 299 } 300 for (int i = 0; i < mTouchRects.size(); i++) { 301 Rect r = mTouchRects.get(i); 302 if (r.contains((int) x, (int) y)) { 303 return true; 304 } 305 } 306 float topLineX = x - mTopLineView.getX(); 307 float topLineY = y - mTopLineView.getY(); 308 return mTopLineView.isInTouchRect(topLineX, topLineY); 309 } 310 } 311 getFirstChildNotGone()312 private View getFirstChildNotGone() { 313 for (int i = 0; i < getChildCount(); i++) { 314 final View child = getChildAt(i); 315 if (child.getVisibility() != GONE) { 316 return child; 317 } 318 } 319 return this; 320 } 321 322 @Override hasOverlappingRendering()323 public boolean hasOverlappingRendering() { 324 return false; 325 } 326 isInTouchRect(float x, float y)327 public boolean isInTouchRect(float x, float y) { 328 if (mExpandClickListener == null) { 329 return false; 330 } 331 return mTouchListener.isInside(x, y); 332 } 333 334 /** 335 * Sets whether or not all touches to this header view will register as a click. Note that 336 * if the config value for {@code config_notificationHeaderClickableForExpand} is {@code true}, 337 * then calling this method with {@code false} will not override that configuration. 338 */ 339 @RemotableViewMethod setAcceptAllTouches(boolean acceptAllTouches)340 public void setAcceptAllTouches(boolean acceptAllTouches) { 341 mAcceptAllTouches = mEntireHeaderClickable || acceptAllTouches; 342 } 343 344 /** 345 * Sets whether only the expand icon itself should serve as the expand target. 346 */ 347 @RemotableViewMethod setExpandOnlyOnButton(boolean expandOnlyOnButton)348 public void setExpandOnlyOnButton(boolean expandOnlyOnButton) { 349 mExpandOnlyOnButton = expandOnlyOnButton; 350 } 351 } 352