Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2016 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 com.android.internal.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.TypedArray;
     21 import android.graphics.drawable.RippleDrawable;
     22 import android.util.AttributeSet;
     23 import android.util.Pair;
     24 import android.view.Gravity;
     25 import android.view.RemotableViewMethod;
     26 import android.view.View;
     27 import android.view.ViewGroup;
     28 import android.widget.LinearLayout;
     29 import android.widget.RemoteViews;
     30 import android.widget.TextView;
     31 
     32 import java.util.ArrayList;
     33 import java.util.Comparator;
     34 
     35 /**
     36  * Layout for notification actions that ensures that no action consumes more than their share of
     37  * the remaining available width, and the last action consumes the remaining space.
     38  */
     39 @RemoteViews.RemoteView
     40 public class NotificationActionListLayout extends LinearLayout {
     41 
     42     private final int mGravity;
     43     private int mTotalWidth = 0;
     44     private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>();
     45     private ArrayList<View> mMeasureOrderOther = new ArrayList<>();
     46     private boolean mEmphasizedMode;
     47     private int mDefaultPaddingBottom;
     48     private int mDefaultPaddingTop;
     49     private int mEmphasizedHeight;
     50     private int mRegularHeight;
     51 
     52     public NotificationActionListLayout(Context context, AttributeSet attrs) {
     53         this(context, attrs, 0);
     54     }
     55 
     56     public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr) {
     57         this(context, attrs, defStyleAttr, 0);
     58     }
     59 
     60     public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
     61         super(context, attrs, defStyleAttr, defStyleRes);
     62 
     63         int[] attrIds = { android.R.attr.gravity };
     64         TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes);
     65         mGravity = ta.getInt(0, 0);
     66         ta.recycle();
     67     }
     68 
     69     @Override
     70     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     71         if (mEmphasizedMode) {
     72             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     73             return;
     74         }
     75         final int N = getChildCount();
     76         int textViews = 0;
     77         int otherViews = 0;
     78         int notGoneChildren = 0;
     79 
     80         for (int i = 0; i < N; i++) {
     81             View c = getChildAt(i);
     82             if (c instanceof TextView) {
     83                 textViews++;
     84             } else {
     85                 otherViews++;
     86             }
     87             if (c.getVisibility() != GONE) {
     88                 notGoneChildren++;
     89             }
     90         }
     91 
     92         // Rebuild the measure order if the number of children changed or the text length of
     93         // any of the children changed.
     94         boolean needRebuild = false;
     95         if (textViews != mMeasureOrderTextViews.size()
     96                 || otherViews != mMeasureOrderOther.size()) {
     97             needRebuild = true;
     98         }
     99         if (!needRebuild) {
    100             final int size = mMeasureOrderTextViews.size();
    101             for (int i = 0; i < size; i++) {
    102                 Pair<Integer, TextView> pair = mMeasureOrderTextViews.get(i);
    103                 if (pair.first != pair.second.getText().length()) {
    104                     needRebuild = true;
    105                 }
    106             }
    107         }
    108 
    109         if (needRebuild) {
    110             rebuildMeasureOrder(textViews, otherViews);
    111         }
    112 
    113         final boolean constrained =
    114                 MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED;
    115 
    116         final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
    117         final int otherSize = mMeasureOrderOther.size();
    118         int usedWidth = 0;
    119 
    120         int measuredChildren = 0;
    121         for (int i = 0; i < N; i++) {
    122             // Measure shortest children first. To avoid measuring twice, we approximate by looking
    123             // at the text length.
    124             View c;
    125             if (i < otherSize) {
    126                 c = mMeasureOrderOther.get(i);
    127             } else {
    128                 c = mMeasureOrderTextViews.get(i - otherSize).second;
    129             }
    130             if (c.getVisibility() == GONE) {
    131                 continue;
    132             }
    133             MarginLayoutParams lp = (MarginLayoutParams) c.getLayoutParams();
    134 
    135             int usedWidthForChild = usedWidth;
    136             if (constrained) {
    137                 // Make sure that this child doesn't consume more than its share of the remaining
    138                 // total available space. Not used space will benefit subsequent views. Since we
    139                 // measure in the order of (approx.) size, a large view can still take more than its
    140                 // share if the others are small.
    141                 int availableWidth = innerWidth - usedWidth;
    142                 int maxWidthForChild = availableWidth / (notGoneChildren - measuredChildren);
    143 
    144                 usedWidthForChild = innerWidth - maxWidthForChild;
    145             }
    146 
    147             measureChildWithMargins(c, widthMeasureSpec, usedWidthForChild,
    148                     heightMeasureSpec, 0 /* usedHeight */);
    149 
    150             usedWidth += c.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
    151             measuredChildren++;
    152         }
    153 
    154         mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft;
    155         setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    156                 resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    157     }
    158 
    159     private void rebuildMeasureOrder(int capacityText, int capacityOther) {
    160         clearMeasureOrder();
    161         mMeasureOrderTextViews.ensureCapacity(capacityText);
    162         mMeasureOrderOther.ensureCapacity(capacityOther);
    163         final int childCount = getChildCount();
    164         for (int i = 0; i < childCount; i++) {
    165             View c = getChildAt(i);
    166             if (c instanceof TextView && ((TextView) c).getText().length() > 0) {
    167                 mMeasureOrderTextViews.add(Pair.create(((TextView) c).getText().length(),
    168                         (TextView)c));
    169             } else {
    170                 mMeasureOrderOther.add(c);
    171             }
    172         }
    173         mMeasureOrderTextViews.sort(MEASURE_ORDER_COMPARATOR);
    174     }
    175 
    176     private void clearMeasureOrder() {
    177         mMeasureOrderOther.clear();
    178         mMeasureOrderTextViews.clear();
    179     }
    180 
    181     @Override
    182     public void onViewAdded(View child) {
    183         super.onViewAdded(child);
    184         clearMeasureOrder();
    185         // For some reason ripples + notification actions seem to be an unhappy combination
    186         // b/69474443 so just turn them off for now.
    187         if (child.getBackground() instanceof RippleDrawable) {
    188             ((RippleDrawable)child.getBackground()).setForceSoftware(true);
    189         }
    190     }
    191 
    192     @Override
    193     public void onViewRemoved(View child) {
    194         super.onViewRemoved(child);
    195         clearMeasureOrder();
    196     }
    197 
    198     @Override
    199     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    200         if (mEmphasizedMode) {
    201             super.onLayout(changed, left, top, right, bottom);
    202             return;
    203         }
    204         final boolean isLayoutRtl = isLayoutRtl();
    205         final int paddingTop = mPaddingTop;
    206         final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
    207 
    208         int childTop;
    209         int childLeft;
    210         if (centerAligned) {
    211             childLeft = mPaddingLeft + left + (right - left) / 2 - mTotalWidth / 2;
    212         } else {
    213             childLeft = mPaddingLeft;
    214             int absoluteGravity = Gravity.getAbsoluteGravity(Gravity.START, getLayoutDirection());
    215             if (absoluteGravity == Gravity.RIGHT) {
    216                 childLeft += right - left - mTotalWidth;
    217             }
    218         }
    219 
    220 
    221         // Where bottom of child should go
    222         final int height = bottom - top;
    223 
    224         // Space available for child
    225         int innerHeight = height - paddingTop - mPaddingBottom;
    226 
    227         final int count = getChildCount();
    228 
    229         int start = 0;
    230         int dir = 1;
    231         //In case of RTL, start drawing from the last child.
    232         if (isLayoutRtl) {
    233             start = count - 1;
    234             dir = -1;
    235         }
    236 
    237         for (int i = 0; i < count; i++) {
    238             final int childIndex = start + dir * i;
    239             final View child = getChildAt(childIndex);
    240             if (child.getVisibility() != GONE) {
    241                 final int childWidth = child.getMeasuredWidth();
    242                 final int childHeight = child.getMeasuredHeight();
    243 
    244                 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    245 
    246                 childTop = paddingTop + ((innerHeight - childHeight) / 2)
    247                             + lp.topMargin - lp.bottomMargin;
    248 
    249                 childLeft += lp.leftMargin;
    250                 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
    251                 childLeft += childWidth + lp.rightMargin;
    252             }
    253         }
    254     }
    255 
    256     @Override
    257     protected void onFinishInflate() {
    258         super.onFinishInflate();
    259         mDefaultPaddingBottom = getPaddingBottom();
    260         mDefaultPaddingTop = getPaddingTop();
    261         updateHeights();
    262     }
    263 
    264     private void updateHeights() {
    265         int paddingTop = getResources().getDimensionPixelSize(
    266                 com.android.internal.R.dimen.notification_content_margin);
    267         // same padding on bottom and at end
    268         int paddingBottom = getResources().getDimensionPixelSize(
    269                 com.android.internal.R.dimen.notification_content_margin_end);
    270         mEmphasizedHeight = paddingBottom + paddingTop + getResources().getDimensionPixelSize(
    271                 com.android.internal.R.dimen.notification_action_emphasized_height);
    272         mRegularHeight = getResources().getDimensionPixelSize(
    273                 com.android.internal.R.dimen.notification_action_list_height);
    274     }
    275 
    276     /**
    277      * Set whether the list is in a mode where some actions are emphasized. This will trigger an
    278      * equal measuring where all actions are full height and change a few parameters like
    279      * the padding.
    280      */
    281     @RemotableViewMethod
    282     public void setEmphasizedMode(boolean emphasizedMode) {
    283         mEmphasizedMode = emphasizedMode;
    284         int height;
    285         if (emphasizedMode) {
    286             int paddingTop = getResources().getDimensionPixelSize(
    287                     com.android.internal.R.dimen.notification_content_margin);
    288             // same padding on bottom and at end
    289             int paddingBottom = getResources().getDimensionPixelSize(
    290                     com.android.internal.R.dimen.notification_content_margin_end);
    291             height = mEmphasizedHeight;
    292             int buttonPaddingInternal = getResources().getDimensionPixelSize(
    293                     com.android.internal.R.dimen.button_inset_vertical_material);
    294             setPaddingRelative(getPaddingStart(),
    295                     paddingTop - buttonPaddingInternal,
    296                     getPaddingEnd(),
    297                     paddingBottom - buttonPaddingInternal);
    298         } else {
    299             setPaddingRelative(getPaddingStart(),
    300                     mDefaultPaddingTop,
    301                     getPaddingEnd(),
    302                     mDefaultPaddingBottom);
    303             height = mRegularHeight;
    304         }
    305         ViewGroup.LayoutParams layoutParams = getLayoutParams();
    306         layoutParams.height = height;
    307         setLayoutParams(layoutParams);
    308     }
    309 
    310     public int getExtraMeasureHeight() {
    311         if (mEmphasizedMode) {
    312             return mEmphasizedHeight - mRegularHeight;
    313         }
    314         return 0;
    315     }
    316 
    317     public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR
    318             = (a, b) -> a.first.compareTo(b.first);
    319 }
    320