/* * Copyright (C) 2016 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 com.android.internal.widget; import static android.widget.flags.Flags.messagingChildRequestLayout; import android.annotation.Nullable; import android.annotation.Px; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.os.Build; import android.os.Trace; import android.util.AttributeSet; import android.view.RemotableViewMethod; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.RemoteViews; import com.android.internal.R; /** * A custom-built layout for the Notification.MessagingStyle. * * Evicts children until they all fit. */ @RemoteViews.RemoteView public class MessagingLinearLayout extends ViewGroup { /** * Spacing to be applied between views. */ private int mSpacing; private int mMaxDisplayedLines = Integer.MAX_VALUE; private static final boolean TRACE_ONMEASURE = Build.isDebuggable(); public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MessagingLinearLayout, 0, 0); final int N = a.getIndexCount(); for (int i = 0; i < N; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.MessagingLinearLayout_spacing: mSpacing = a.getDimensionPixelSize(i, 0); break; } } a.recycle(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (TRACE_ONMEASURE) { Trace.beginSection("MessagingLinearLayout#onMeasure"); trackMeasureSpecs(widthMeasureSpec, heightMeasureSpec); } // This is essentially a bottom-up linear layout that only adds children that fit entirely // up to a maximum height. int targetHeight = MeasureSpec.getSize(heightMeasureSpec); switch (MeasureSpec.getMode(heightMeasureSpec)) { case MeasureSpec.UNSPECIFIED: targetHeight = Integer.MAX_VALUE; break; } // Now that we know which views to take, fix up the indents and see what width we get. int measuredWidth = mPaddingLeft + mPaddingRight; final int count = getChildCount(); int totalHeight; for (int i = 0; i < count; ++i) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); lp.hide = true; // Child always needs to be measured to calculate hide property correctly in onMeasure. if (messagingChildRequestLayout()) { child.requestLayout(); } if (child instanceof MessagingChild) { MessagingChild messagingChild = (MessagingChild) child; // Whenever we encounter the message first, it's always first in the layout messagingChild.setIsFirstInLayout(true); } } totalHeight = mPaddingTop + mPaddingBottom; boolean first = true; int linesRemaining = mMaxDisplayedLines; // Starting from the bottom: we measure every view as if it were the only one. If it still // fits, we take it, otherwise we stop there. MessagingChild previousChild = null; View previousView = null; int previousChildHeight = 0; int previousTotalHeight = 0; int previousLinesConsumed = 0; for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) { if (getChildAt(i).getVisibility() == GONE) { continue; } final View child = getChildAt(i); LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); MessagingChild messagingChild = null; int spacing = mSpacing; int previousChildIncrease = 0; if (child instanceof MessagingChild) { // We need to remeasure the previous child again if it's not the first anymore if (previousChild != null && previousChild.hasDifferentHeightWhenFirst()) { previousChild.setIsFirstInLayout(false); measureChildWithMargins(previousView, widthMeasureSpec, 0, heightMeasureSpec, previousTotalHeight - previousChildHeight); previousChildIncrease = previousView.getMeasuredHeight() - previousChildHeight; linesRemaining -= previousChild.getConsumedLines() - previousLinesConsumed; } messagingChild = (MessagingChild) child; messagingChild.setMaxDisplayedLines(linesRemaining); spacing += messagingChild.getExtraSpacing(); } spacing = first ? 0 : spacing; measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight - mPaddingTop - mPaddingBottom + spacing); final int childHeight = child.getMeasuredHeight(); int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin + lp.bottomMargin + spacing + previousChildIncrease); int measureType = MessagingChild.MEASURED_NORMAL; if (messagingChild != null) { measureType = messagingChild.getMeasuredType(); } // We never measure the first item as too small, we want to at least show something. boolean isTooSmall = measureType == MessagingChild.MEASURED_TOO_SMALL && !first; boolean isShortened = measureType == MessagingChild.MEASURED_SHORTENED || measureType == MessagingChild.MEASURED_TOO_SMALL && first; boolean showView = newHeight <= targetHeight && !isTooSmall; if (showView) { if (messagingChild != null) { previousLinesConsumed = messagingChild.getConsumedLines(); linesRemaining -= previousLinesConsumed; previousChild = messagingChild; previousView = child; previousChildHeight = childHeight; previousTotalHeight = totalHeight; } totalHeight = newHeight; measuredWidth = Math.max(measuredWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + mPaddingLeft + mPaddingRight); lp.hide = false; if (isShortened || linesRemaining <= 0) { break; } } else { // We now became too short, let's make sure to reset any previous views to be first // and remeasure it. if (previousChild != null && previousChild.hasDifferentHeightWhenFirst()) { previousChild.setIsFirstInLayout(true); // We need to remeasure the previous child again since it became first measureChildWithMargins(previousView, widthMeasureSpec, 0, heightMeasureSpec, previousTotalHeight - previousChildHeight); // The totalHeight is already correct here since we only set it during the // first pass } break; } first = false; } setMeasuredDimension( resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), widthMeasureSpec), Math.max(getSuggestedMinimumHeight(), totalHeight)); if (TRACE_ONMEASURE) { Trace.endSection(); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; int childTop; // Where right end of child should go final int width = right - left; final int childRight = width - mPaddingRight; final int layoutDirection = getLayoutDirection(); final int count = getChildCount(); childTop = mPaddingTop; boolean first = true; final boolean shown = isShown(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); MessagingChild messagingChild = (MessagingChild) child; final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); int childLeft; if (layoutDirection == LAYOUT_DIRECTION_RTL) { childLeft = childRight - childWidth - lp.rightMargin; } else { childLeft = paddingLeft + lp.leftMargin; } if (lp.hide) { if (shown && lp.visibleBefore) { // We still want to lay out the child to have great animations child.layout(childLeft, childTop, childLeft + childWidth, childTop + lp.lastVisibleHeight); messagingChild.hideAnimated(); } lp.visibleBefore = false; continue; } else { lp.visibleBefore = true; lp.lastVisibleHeight = childHeight; } if (!first) { childTop += mSpacing; } childTop += lp.topMargin; child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); childTop += childHeight + lp.bottomMargin; first = false; } } private void trackMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec) { if (!TRACE_ONMEASURE) { return; } final int availableWidth = MeasureSpec.getSize(widthMeasureSpec); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int availableHeight = MeasureSpec.getSize(heightMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); Trace.setCounter("MessagingLinearLayout#onMeasure_widthMeasureSpecSize", availableWidth); Trace.setCounter("MessagingLinearLayout#onMeasure_widthMeasureSpecMode", widthMode); Trace.setCounter("MessagingLinearLayout#onMeasure_heightMeasureSpecSize", availableHeight); Trace.setCounter("MessagingLinearLayout#onMeasure_heightMeasureSpecMode", heightMode); } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.hide) { MessagingChild messagingChild = (MessagingChild) child; if (!messagingChild.isHidingAnimated()) { return true; } } return super.drawChild(canvas, child, drawingTime); } /** * Set the spacing to be applied between views. */ public void setSpacing(@Px int spacing) { if (mSpacing != spacing) { mSpacing = spacing; requestLayout(); } } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(mContext, attrs); } @Override protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } @Override protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { LayoutParams copy = new LayoutParams(lp.width, lp.height); if (lp instanceof MarginLayoutParams) { copy.copyMarginsFrom((MarginLayoutParams) lp); } return copy; } public static boolean isGone(View view) { if (view.getVisibility() == View.GONE) { return true; } final ViewGroup.LayoutParams lp = view.getLayoutParams(); if (lp instanceof MessagingLinearLayout.LayoutParams && ((MessagingLinearLayout.LayoutParams) lp).hide) { return true; } return false; } /** * Sets how many lines should be displayed at most */ @RemotableViewMethod public void setMaxDisplayedLines(int numberLines) { mMaxDisplayedLines = numberLines; } public IMessagingLayout getMessagingLayout() { View view = this; while (true) { ViewParent p = view.getParent(); if (p instanceof View) { view = (View) p; if (view instanceof IMessagingLayout) { return (IMessagingLayout) view; } } else { return null; } } } @Override public int getBaseline() { // When placed in a horizontal linear layout (as is the case in a single-line MessageGroup), // align with the last visible child (which is the one that will be displayed in the single- // line group. int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View child = getChildAt(i); if (isGone(child)) { continue; } final int childBaseline = child.getBaseline(); if (childBaseline == -1) { return -1; } MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); return lp.topMargin + childBaseline; } return super.getBaseline(); } public interface MessagingChild { int MEASURED_NORMAL = 0; int MEASURED_SHORTENED = 1; int MEASURED_TOO_SMALL = 2; int getMeasuredType(); int getConsumedLines(); void setMaxDisplayedLines(int lines); void hideAnimated(); boolean isHidingAnimated(); /** * Set that this view is first in layout. Relevant and only set if * {@link #hasDifferentHeightWhenFirst()}. * @param first is this first? */ default void setIsFirstInLayout(boolean first) {} /** * @return if this layout has different height it is first in the layout */ default boolean hasDifferentHeightWhenFirst() { return false; } default int getExtraSpacing() { return 0; } void recycle(); } public static class LayoutParams extends MarginLayoutParams { public boolean hide = false; public boolean visibleBefore = false; public int lastVisibleHeight; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } public LayoutParams(int width, int height) { super(width, height); } } }