| /* |
| * 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<View> 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(); |
| } |
| } |
| } |