| /* |
| * Copyright (C) 2015 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 android.view; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Outline; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build; |
| import android.util.AttributeSet; |
| import android.widget.RelativeLayout; |
| import android.widget.RemoteViews; |
| import android.widget.TextView; |
| |
| import com.android.internal.R; |
| import com.android.internal.widget.CachingIconView; |
| import com.android.internal.widget.NotificationExpandButton; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * A header of a notification view |
| * |
| * @hide |
| */ |
| @RemoteViews.RemoteView |
| public class NotificationHeaderView extends RelativeLayout { |
| private final int mTouchableHeight; |
| private OnClickListener mExpandClickListener; |
| private HeaderTouchListener mTouchListener = new HeaderTouchListener(); |
| private NotificationTopLineView mTopLineView; |
| private NotificationExpandButton mExpandButton; |
| private View mAltExpandTarget; |
| private CachingIconView mIcon; |
| private Drawable mBackground; |
| private boolean mEntireHeaderClickable; |
| private boolean mExpandOnlyOnButton; |
| private boolean mAcceptAllTouches; |
| |
| ViewOutlineProvider mProvider = new ViewOutlineProvider() { |
| @Override |
| public void getOutline(View view, Outline outline) { |
| if (mBackground != null) { |
| outline.setRect(0, 0, getWidth(), getHeight()); |
| outline.setAlpha(1f); |
| } |
| } |
| }; |
| |
| public NotificationHeaderView(Context context) { |
| this(context, null); |
| } |
| |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { |
| this(context, attrs, defStyleAttr, 0); |
| } |
| |
| public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, |
| int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| Resources res = getResources(); |
| mTouchableHeight = res.getDimensionPixelSize(R.dimen.notification_header_touchable_height); |
| mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mIcon = findViewById(R.id.icon); |
| mTopLineView = findViewById(R.id.notification_top_line); |
| mExpandButton = findViewById(R.id.expand_button); |
| mAltExpandTarget = findViewById(R.id.alternate_expand_target); |
| setClipToPadding(false); |
| } |
| |
| /** |
| * Set a {@link Drawable} to be displayed as a background on the header. |
| */ |
| public void setHeaderBackgroundDrawable(Drawable drawable) { |
| if (drawable != null) { |
| setWillNotDraw(false); |
| mBackground = drawable; |
| mBackground.setCallback(this); |
| setOutlineProvider(mProvider); |
| } else { |
| setWillNotDraw(true); |
| mBackground = null; |
| setOutlineProvider(null); |
| } |
| invalidate(); |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| if (mBackground != null) { |
| mBackground.setBounds(0, 0, getWidth(), getHeight()); |
| mBackground.draw(canvas); |
| } |
| } |
| |
| @Override |
| protected boolean verifyDrawable(@NonNull Drawable who) { |
| return super.verifyDrawable(who) || who == mBackground; |
| } |
| |
| @Override |
| protected void drawableStateChanged() { |
| if (mBackground != null && mBackground.isStateful()) { |
| mBackground.setState(getDrawableState()); |
| } |
| } |
| |
| private void updateTouchListener() { |
| if (mExpandClickListener == null) { |
| setOnTouchListener(null); |
| return; |
| } |
| setOnTouchListener(mTouchListener); |
| mTouchListener.bindTouchRects(); |
| } |
| |
| @Override |
| public void setOnClickListener(@Nullable OnClickListener l) { |
| mExpandClickListener = l; |
| mExpandButton.setOnClickListener(mExpandClickListener); |
| mAltExpandTarget.setOnClickListener(mExpandClickListener); |
| updateTouchListener(); |
| } |
| |
| /** |
| * Sets the extra margin at the end of the top line of left-aligned text + icons. |
| * This value will have the margin required to accommodate the expand button added to it. |
| * |
| * @param extraMarginEnd extra margin in px |
| */ |
| public void setTopLineExtraMarginEnd(int extraMarginEnd) { |
| mTopLineView.setHeaderTextMarginEnd(extraMarginEnd); |
| } |
| |
| /** |
| * Sets the extra margin at the end of the top line of left-aligned text + icons. |
| * This value will have the margin required to accommodate the expand button added to it. |
| * |
| * @param extraMarginEndDp extra margin in dp |
| */ |
| @RemotableViewMethod |
| public void setTopLineExtraMarginEndDp(float extraMarginEndDp) { |
| setTopLineExtraMarginEnd( |
| (int) (extraMarginEndDp * getResources().getDisplayMetrics().density)); |
| } |
| |
| /** |
| * This is used to make the low-priority header show the bolded text of a title. |
| * |
| * @param styleTextAsTitle true if this header's text is to have the style of a title |
| */ |
| @RemotableViewMethod |
| public void styleTextAsTitle(boolean styleTextAsTitle) { |
| int styleResId = styleTextAsTitle |
| ? R.style.TextAppearance_DeviceDefault_Notification_Title |
| : R.style.TextAppearance_DeviceDefault_Notification_Info; |
| // Most of the time, we're showing text in the minimized state |
| View headerText = findViewById(R.id.header_text); |
| if (headerText instanceof TextView) { |
| ((TextView) headerText).setTextAppearance(styleResId); |
| } |
| // If there's no summary or text, we show the app name instead of nothing |
| View appNameText = findViewById(R.id.app_name_text); |
| if (appNameText instanceof TextView) { |
| ((TextView) appNameText).setTextAppearance(styleResId); |
| } |
| } |
| |
| /** |
| * Handles clicks on the header based on the region tapped. |
| */ |
| public class HeaderTouchListener implements OnTouchListener { |
| |
| private final ArrayList<Rect> mTouchRects = new ArrayList<>(); |
| private Rect mExpandButtonRect; |
| private Rect mAltExpandTargetRect; |
| private int mTouchSlop; |
| private boolean mTrackGesture; |
| private float mDownX; |
| private float mDownY; |
| |
| public HeaderTouchListener() { |
| } |
| |
| public void bindTouchRects() { |
| mTouchRects.clear(); |
| addRectAroundView(mIcon); |
| mExpandButtonRect = addRectAroundView(mExpandButton); |
| mAltExpandTargetRect = addRectAroundView(mAltExpandTarget); |
| addWidthRect(); |
| mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); |
| } |
| |
| private void addWidthRect() { |
| Rect r = new Rect(); |
| r.top = 0; |
| r.bottom = mTouchableHeight; |
| r.left = 0; |
| r.right = getWidth(); |
| mTouchRects.add(r); |
| } |
| |
| private Rect addRectAroundView(View view) { |
| final Rect r = getRectAroundView(view); |
| mTouchRects.add(r); |
| return r; |
| } |
| |
| private Rect getRectAroundView(View view) { |
| float size = 48 * getResources().getDisplayMetrics().density; |
| float width = Math.max(size, view.getWidth()); |
| float height = Math.max(size, view.getHeight()); |
| final Rect r = new Rect(); |
| if (view.getVisibility() == GONE) { |
| view = getFirstChildNotGone(); |
| r.left = (int) (view.getLeft() - width / 2.0f); |
| } else { |
| r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f); |
| } |
| r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f); |
| r.bottom = (int) (r.top + height); |
| r.right = (int) (r.left + width); |
| return r; |
| } |
| |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| float x = event.getX(); |
| float y = event.getY(); |
| switch (event.getActionMasked() & MotionEvent.ACTION_MASK) { |
| case MotionEvent.ACTION_DOWN: |
| mTrackGesture = false; |
| if (isInside(x, y)) { |
| mDownX = x; |
| mDownY = y; |
| mTrackGesture = true; |
| return true; |
| } |
| break; |
| case MotionEvent.ACTION_MOVE: |
| if (mTrackGesture) { |
| if (Math.abs(mDownX - x) > mTouchSlop |
| || Math.abs(mDownY - y) > mTouchSlop) { |
| mTrackGesture = false; |
| } |
| } |
| break; |
| case MotionEvent.ACTION_UP: |
| if (mTrackGesture) { |
| float topLineX = mTopLineView.getX(); |
| float topLineY = mTopLineView.getY(); |
| if (mTopLineView.onTouchUp(x - topLineX, y - topLineY, |
| mDownX - topLineX, mDownY - topLineY)) { |
| break; |
| } |
| mExpandButton.performClick(); |
| } |
| break; |
| } |
| return mTrackGesture; |
| } |
| |
| private boolean isInside(float x, float y) { |
| if (mAcceptAllTouches) { |
| return true; |
| } |
| if (mExpandOnlyOnButton) { |
| return mExpandButtonRect.contains((int) x, (int) y) |
| || mAltExpandTargetRect.contains((int) x, (int) y); |
| } |
| for (int i = 0; i < mTouchRects.size(); i++) { |
| Rect r = mTouchRects.get(i); |
| if (r.contains((int) x, (int) y)) { |
| return true; |
| } |
| } |
| float topLineX = x - mTopLineView.getX(); |
| float topLineY = y - mTopLineView.getY(); |
| return mTopLineView.isInTouchRect(topLineX, topLineY); |
| } |
| } |
| |
| private View getFirstChildNotGone() { |
| for (int i = 0; i < getChildCount(); i++) { |
| final View child = getChildAt(i); |
| if (child.getVisibility() != GONE) { |
| return child; |
| } |
| } |
| return this; |
| } |
| |
| @Override |
| public boolean hasOverlappingRendering() { |
| return false; |
| } |
| |
| public boolean isInTouchRect(float x, float y) { |
| if (mExpandClickListener == null) { |
| return false; |
| } |
| return mTouchListener.isInside(x, y); |
| } |
| |
| /** |
| * Sets whether or not all touches to this header view will register as a click. Note that |
| * if the config value for {@code config_notificationHeaderClickableForExpand} is {@code true}, |
| * then calling this method with {@code false} will not override that configuration. |
| */ |
| @RemotableViewMethod |
| public void setAcceptAllTouches(boolean acceptAllTouches) { |
| mAcceptAllTouches = mEntireHeaderClickable || acceptAllTouches; |
| } |
| |
| /** |
| * Sets whether only the expand icon itself should serve as the expand target. |
| */ |
| @RemotableViewMethod |
| public void setExpandOnlyOnButton(boolean expandOnlyOnButton) { |
| mExpandOnlyOnButton = expandOnlyOnButton; |
| } |
| } |