/* * 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.Nullable; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Rect; import android.os.Trace; import android.util.AttributeSet; import android.widget.RemoteViews; import com.android.internal.R; import java.util.HashSet; import java.util.Set; /** * The top line of content in a notification view. * This includes the text views and badges but excludes the icon and the expander. * * @hide */ @RemoteViews.RemoteView public class NotificationTopLineView extends ViewGroup { private final OverflowAdjuster mOverflowAdjuster = new OverflowAdjuster(); private final int mGravityY; private final int mChildMinWidth; private final int mChildHideWidth; @Nullable private View mAppName; @Nullable private View mTitle; private View mHeaderText; private View mHeaderTextDivider; private View mSecondaryHeaderText; private View mSecondaryHeaderTextDivider; private OnClickListener mFeedbackListener; private HeaderTouchListener mTouchListener = new HeaderTouchListener(); private View mFeedbackIcon; private int mHeaderTextMarginEnd; private Set mViewsToDisappear = new HashSet<>(); private int mMaxAscent; private int mMaxDescent; public NotificationTopLineView(Context context) { this(context, null); } public NotificationTopLineView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public NotificationTopLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public NotificationTopLineView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); Resources res = getResources(); mChildMinWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_min_width); mChildHideWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_hide_width); // NOTE: Implementation only supports TOP, BOTTOM, and CENTER_VERTICAL gravities, // with CENTER_VERTICAL being the default. int[] attrIds = {android.R.attr.gravity}; TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes); int gravity = ta.getInt(0, 0); ta.recycle(); if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { mGravityY = Gravity.BOTTOM; } else if ((gravity & Gravity.TOP) == Gravity.TOP) { mGravityY = Gravity.TOP; } else { mGravityY = Gravity.CENTER_VERTICAL; } } @Override protected void onFinishInflate() { super.onFinishInflate(); mAppName = findViewById(R.id.app_name_text); mTitle = findViewById(R.id.title); mHeaderText = findViewById(R.id.header_text); mHeaderTextDivider = findViewById(R.id.header_text_divider); mSecondaryHeaderText = findViewById(R.id.header_text_secondary); mSecondaryHeaderTextDivider = findViewById(R.id.header_text_secondary_divider); mFeedbackIcon = findViewById(R.id.feedback); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Trace.beginSection("NotificationTopLineView#onMeasure"); final int givenWidth = MeasureSpec.getSize(widthMeasureSpec); final int givenHeight = MeasureSpec.getSize(heightMeasureSpec); final boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST; int wrapContentWidthSpec = MeasureSpec.makeMeasureSpec(givenWidth, MeasureSpec.AT_MOST); int heightSpec = MeasureSpec.makeMeasureSpec(givenHeight, MeasureSpec.AT_MOST); int totalWidth = getPaddingStart(); int maxChildHeight = -1; mMaxAscent = -1; mMaxDescent = -1; for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { // We'll give it the rest of the space in the end continue; } final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidthSpec = getChildMeasureSpec(wrapContentWidthSpec, lp.leftMargin + lp.rightMargin, lp.width); int childHeightSpec = getChildMeasureSpec(heightSpec, lp.topMargin + lp.bottomMargin, lp.height); child.measure(childWidthSpec, childHeightSpec); totalWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth(); int childBaseline = child.getBaseline(); int childHeight = child.getMeasuredHeight(); if (childBaseline != -1) { mMaxAscent = Math.max(mMaxAscent, childBaseline); mMaxDescent = Math.max(mMaxDescent, childHeight - childBaseline); } maxChildHeight = Math.max(maxChildHeight, childHeight); } mViewsToDisappear.clear(); // Ensure that there is at least enough space for the icons int endMargin = Math.max(mHeaderTextMarginEnd, getPaddingEnd()); if (totalWidth > givenWidth - endMargin) { int overFlow = totalWidth - givenWidth + endMargin; mOverflowAdjuster.resetForOverflow(overFlow, heightSpec) // First shrink the app name, down to a minimum size .adjust(mAppName, null, mChildMinWidth) // Next, shrink the header text (this usually has subText) // This shrinks the subtext first, but not all the way (yet!) .adjust(mHeaderText, mHeaderTextDivider, mChildMinWidth) // Next, shrink the secondary header text (this rarely has conversationTitle) .adjust(mSecondaryHeaderText, mSecondaryHeaderTextDivider, 0) // Next, shrink the title text (this has contentTitle; only in headerless views) .adjust(mTitle, null, mChildMinWidth) // Next, shrink the header down to 0 if still necessary. .adjust(mHeaderText, mHeaderTextDivider, 0) // Finally, shrink the title to 0 if necessary (media is super cramped) .adjust(mTitle, null, 0) // Clean up .finish(); } setMeasuredDimension(givenWidth, wrapHeight ? maxChildHeight : givenHeight); Trace.endSection(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; final int width = getWidth(); int start = getPaddingStart(); int childCount = getChildCount(); int ownHeight = b - t; int childSpace = ownHeight - mPaddingTop - mPaddingBottom; // Instead of centering the baseline, pick a baseline that centers views which align to it. // Only used when mGravityY is CENTER_VERTICAL int baselineY = mPaddingTop + ((childSpace - (mMaxAscent + mMaxDescent)) / 2) + mMaxAscent; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } int childHeight = child.getMeasuredHeight(); MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams(); // Calculate vertical alignment of the views, accounting for the view baselines int childTop; int childBaseline = child.getBaseline(); switch (mGravityY) { case Gravity.TOP: childTop = mPaddingTop + params.topMargin; if (childBaseline != -1) { childTop += mMaxAscent - childBaseline; } break; case Gravity.CENTER_VERTICAL: if (childBaseline != -1) { // Align baselines vertically only if the child is smaller than us if (childSpace - childHeight > 0) { childTop = baselineY - childBaseline; } else { childTop = mPaddingTop + (childSpace - childHeight) / 2; } } else { childTop = mPaddingTop + ((childSpace - childHeight) / 2) + params.topMargin - params.bottomMargin; } break; case Gravity.BOTTOM: int childBottom = ownHeight - mPaddingBottom; childTop = childBottom - childHeight - params.bottomMargin; if (childBaseline != -1) { int descent = childHeight - childBaseline; childTop -= (mMaxDescent - descent); } break; default: childTop = mPaddingTop; } if (mViewsToDisappear.contains(child)) { child.layout(start, childTop, start, childTop + childHeight); } else { start += params.getMarginStart(); int end = start + child.getMeasuredWidth(); int layoutLeft = isRtl ? width - end : start; int layoutRight = isRtl ? width - start : end; start = end + params.getMarginEnd(); child.layout(layoutLeft, childTop, layoutRight, childTop + childHeight); } } updateTouchListener(); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } private void updateTouchListener() { if (mFeedbackListener == null) { setOnTouchListener(null); return; } setOnTouchListener(mTouchListener); mTouchListener.bindTouchRects(); } /** * Sets onclick listener for feedback icon. */ public void setFeedbackOnClickListener(OnClickListener l) { mFeedbackListener = l; mFeedbackIcon.setOnClickListener(mFeedbackListener); updateTouchListener(); } /** * Sets the margin end for the text portion of the header, excluding right-aligned elements * * @param headerTextMarginEnd margin size */ public void setHeaderTextMarginEnd(int headerTextMarginEnd) { if (mHeaderTextMarginEnd != headerTextMarginEnd) { mHeaderTextMarginEnd = headerTextMarginEnd; requestLayout(); } } /** * Get the current margin end value for the header text * * @return margin size */ public int getHeaderTextMarginEnd() { return mHeaderTextMarginEnd; } /** * Set padding at the start of the view. */ public void setPaddingStart(int paddingStart) { setPaddingRelative(paddingStart, getPaddingTop(), getPaddingEnd(), getPaddingBottom()); } private class HeaderTouchListener implements OnTouchListener { private Rect mFeedbackRect; private int mTouchSlop; private boolean mTrackGesture; private float mDownX; private float mDownY; HeaderTouchListener() { } public void bindTouchRects() { mFeedbackRect = getRectAroundView(mFeedbackIcon); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } 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 && onTouchUp(x, y, mDownX, mDownY)) { return true; } break; } return mTrackGesture; } private boolean onTouchUp(float upX, float upY, float downX, float downY) { if (mFeedbackIcon.isVisibleToUser() && (mFeedbackRect.contains((int) upX, (int) upY) || mFeedbackRect.contains((int) downX, (int) downY))) { mFeedbackIcon.performClick(); return true; } return false; } private boolean isInside(float x, float y) { return mFeedbackRect.contains((int) x, (int) y); } } 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; } /** * Determine if the given point is touching an active part of the top line. */ public boolean isInTouchRect(float x, float y) { if (mFeedbackListener == null) { return false; } return mTouchListener.isInside(x, y); } /** * Perform a click on an active part of the top line, if touching. */ public boolean onTouchUp(float upX, float upY, float downX, float downY) { if (mFeedbackListener == null) { return false; } return mTouchListener.onTouchUp(upX, upY, downX, downY); } private final class OverflowAdjuster { private int mOverflow; private int mHeightSpec; private View mRegrowView; OverflowAdjuster resetForOverflow(int overflow, int heightSpec) { mOverflow = overflow; mHeightSpec = heightSpec; mRegrowView = null; return this; } /** * Shrink the targetView's width by up to overFlow, down to minimumWidth. * @param targetView the view to shrink the width of * @param targetDivider a divider view which should be set to 0 width if the targetView is * @param minimumWidth the minimum width allowed for the targetView * @return this object */ OverflowAdjuster adjust(View targetView, View targetDivider, int minimumWidth) { if (mOverflow <= 0 || targetView == null || targetView.getVisibility() == View.GONE) { return this; } final int oldWidth = targetView.getMeasuredWidth(); if (oldWidth <= minimumWidth) { return this; } // we're too big int newSize = Math.max(minimumWidth, oldWidth - mOverflow); if (minimumWidth == 0 && newSize < mChildHideWidth && mRegrowView != null && mRegrowView != targetView) { // View is so small it's better to hide it entirely (and its divider and margins) // so we can give that space back to another previously shrunken view. newSize = 0; } int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST); targetView.measure(childWidthSpec, mHeightSpec); mOverflow -= oldWidth - newSize; if (newSize == 0) { mViewsToDisappear.add(targetView); mOverflow -= getHorizontalMargins(targetView); if (targetDivider != null && targetDivider.getVisibility() != GONE) { mViewsToDisappear.add(targetDivider); int oldDividerWidth = targetDivider.getMeasuredWidth(); int dividerWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.AT_MOST); targetDivider.measure(dividerWidthSpec, mHeightSpec); mOverflow -= (oldDividerWidth + getHorizontalMargins(targetDivider)); } } if (mOverflow < 0 && mRegrowView != null) { // We're now under-flowing, so regrow the last view. final int regrowCurrentSize = mRegrowView.getMeasuredWidth(); final int maxSize = regrowCurrentSize - mOverflow; int regrowWidthSpec = MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST); mRegrowView.measure(regrowWidthSpec, mHeightSpec); finish(); return this; } if (newSize != 0) { // if we shrunk this view (but did not completely hide it) store it for potential // re-growth if we proactively shorten a future view. mRegrowView = targetView; } return this; } void finish() { resetForOverflow(0, 0); } private int getHorizontalMargins(View view) { MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams(); return params.getMarginStart() + params.getMarginEnd(); } } }