Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2014 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.systemui.statusbar;
     18 
     19 import android.content.Context;
     20 import android.graphics.Paint;
     21 import android.graphics.Rect;
     22 import android.util.AttributeSet;
     23 import android.view.View;
     24 import android.view.ViewGroup;
     25 import android.widget.FrameLayout;
     26 
     27 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
     28 
     29 import java.util.ArrayList;
     30 
     31 /**
     32  * An abstract view for expandable views.
     33  */
     34 public abstract class ExpandableView extends FrameLayout {
     35 
     36     protected OnHeightChangedListener mOnHeightChangedListener;
     37     private int mActualHeight;
     38     protected int mClipTopAmount;
     39     private boolean mDark;
     40     private ArrayList<View> mMatchParentViews = new ArrayList<View>();
     41     private static Rect mClipRect = new Rect();
     42     private boolean mWillBeGone;
     43     private int mMinClipTopAmount = 0;
     44     private boolean mClipToActualHeight = true;
     45     private boolean mChangingPosition = false;
     46     private ViewGroup mTransientContainer;
     47 
     48     public ExpandableView(Context context, AttributeSet attrs) {
     49         super(context, attrs);
     50     }
     51 
     52     @Override
     53     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     54         final int givenSize = MeasureSpec.getSize(heightMeasureSpec);
     55         int ownMaxHeight = Integer.MAX_VALUE;
     56         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
     57         if (heightMode != MeasureSpec.UNSPECIFIED && givenSize != 0) {
     58             ownMaxHeight = Math.min(givenSize, ownMaxHeight);
     59         }
     60         int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
     61         int maxChildHeight = 0;
     62         int childCount = getChildCount();
     63         for (int i = 0; i < childCount; i++) {
     64             View child = getChildAt(i);
     65             if (child.getVisibility() == GONE) {
     66                 continue;
     67             }
     68             int childHeightSpec = newHeightSpec;
     69             ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
     70             if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
     71                 if (layoutParams.height >= 0) {
     72                     // An actual height is set
     73                     childHeightSpec = layoutParams.height > ownMaxHeight
     74                         ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
     75                         : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
     76                 }
     77                 child.measure(
     78                         getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
     79                         childHeightSpec);
     80                 int childHeight = child.getMeasuredHeight();
     81                 maxChildHeight = Math.max(maxChildHeight, childHeight);
     82             } else {
     83                 mMatchParentViews.add(child);
     84             }
     85         }
     86         int ownHeight = heightMode == MeasureSpec.EXACTLY
     87                 ? givenSize : Math.min(ownMaxHeight, maxChildHeight);
     88         newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
     89         for (View child : mMatchParentViews) {
     90             child.measure(getChildMeasureSpec(
     91                     widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
     92                     newHeightSpec);
     93         }
     94         mMatchParentViews.clear();
     95         int width = MeasureSpec.getSize(widthMeasureSpec);
     96         setMeasuredDimension(width, ownHeight);
     97     }
     98 
     99     @Override
    100     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    101         super.onLayout(changed, left, top, right, bottom);
    102         updateClipping();
    103     }
    104 
    105     @Override
    106     public boolean pointInView(float localX, float localY, float slop) {
    107         float top = mClipTopAmount;
    108         float bottom = mActualHeight;
    109         return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) &&
    110                 localY < (bottom + slop);
    111     }
    112 
    113     /**
    114      * Sets the actual height of this notification. This is different than the laid out
    115      * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
    116      *
    117      * @param actualHeight The height of this notification.
    118      * @param notifyListeners Whether the listener should be informed about the change.
    119      */
    120     public void setActualHeight(int actualHeight, boolean notifyListeners) {
    121         mActualHeight = actualHeight;
    122         updateClipping();
    123         if (notifyListeners) {
    124             notifyHeightChanged(false  /* needsAnimation */);
    125         }
    126     }
    127 
    128     public void setActualHeight(int actualHeight) {
    129         setActualHeight(actualHeight, true /* notifyListeners */);
    130     }
    131 
    132     /**
    133      * See {@link #setActualHeight}.
    134      *
    135      * @return The current actual height of this notification.
    136      */
    137     public int getActualHeight() {
    138         return mActualHeight;
    139     }
    140 
    141     /**
    142      * @return The maximum height of this notification.
    143      */
    144     public int getMaxContentHeight() {
    145         return getHeight();
    146     }
    147 
    148     /**
    149      * @return The minimum content height of this notification.
    150      */
    151     public int getMinHeight() {
    152         return getHeight();
    153     }
    154 
    155     /**
    156      * @return The collapsed height of this view. Note that this might be different
    157      * than {@link #getMinHeight()} because some elements like groups may have different sizes when
    158      * they are system expanded.
    159      */
    160     public int getCollapsedHeight() {
    161         return getHeight();
    162     }
    163 
    164     /**
    165      * Sets the notification as dimmed. The default implementation does nothing.
    166      *
    167      * @param dimmed Whether the notification should be dimmed.
    168      * @param fade Whether an animation should be played to change the state.
    169      */
    170     public void setDimmed(boolean dimmed, boolean fade) {
    171     }
    172 
    173     /**
    174      * Sets the notification as dark. The default implementation does nothing.
    175      *
    176      * @param dark Whether the notification should be dark.
    177      * @param fade Whether an animation should be played to change the state.
    178      * @param delay If fading, the delay of the animation.
    179      */
    180     public void setDark(boolean dark, boolean fade, long delay) {
    181         mDark = dark;
    182     }
    183 
    184     public boolean isDark() {
    185         return mDark;
    186     }
    187 
    188     /**
    189      * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
    190      * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
    191      * of a stack scroller update such that the updated intrinsic height (which is dependent on
    192      * whether private or public layout is showing) gets taken into account into all layout
    193      * calculations.
    194      */
    195     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
    196     }
    197 
    198     /**
    199      * Sets whether the notification should hide its private contents if it is sensitive.
    200      */
    201     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
    202             long duration) {
    203     }
    204 
    205     /**
    206      * @return The desired notification height.
    207      */
    208     public int getIntrinsicHeight() {
    209         return getHeight();
    210     }
    211 
    212     /**
    213      * Sets the amount this view should be clipped from the top. This is used when an expanded
    214      * notification is scrolling in the top or bottom stack.
    215      *
    216      * @param clipTopAmount The amount of pixels this view should be clipped from top.
    217      */
    218     public void setClipTopAmount(int clipTopAmount) {
    219         mClipTopAmount = clipTopAmount;
    220         updateClipping();
    221     }
    222 
    223     public int getClipTopAmount() {
    224         return mClipTopAmount;
    225     }
    226 
    227     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
    228         mOnHeightChangedListener = listener;
    229     }
    230 
    231     /**
    232      * @return Whether we can expand this views content.
    233      */
    234     public boolean isContentExpandable() {
    235         return false;
    236     }
    237 
    238     public void notifyHeightChanged(boolean needsAnimation) {
    239         if (mOnHeightChangedListener != null) {
    240             mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
    241         }
    242     }
    243 
    244     public boolean isTransparent() {
    245         return false;
    246     }
    247 
    248     /**
    249      * Perform a remove animation on this view.
    250      *
    251      * @param duration The duration of the remove animation.
    252      * @param translationDirection The direction value from [-1 ... 1] indicating in which the
    253      *                             animation should be performed. A value of -1 means that The
    254      *                             remove animation should be performed upwards,
    255      *                             such that the  child appears to be going away to the top. 1
    256      *                             Should mean the opposite.
    257      * @param onFinishedRunnable A runnable which should be run when the animation is finished.
    258      */
    259     public abstract void performRemoveAnimation(long duration, float translationDirection,
    260             Runnable onFinishedRunnable);
    261 
    262     public abstract void performAddAnimation(long delay, long duration);
    263 
    264     public void setBelowSpeedBump(boolean below) {
    265     }
    266 
    267     /**
    268      * Sets the translation of the view.
    269      */
    270     public void setTranslation(float translation) {
    271         setTranslationX(translation);
    272     }
    273 
    274     /**
    275      * Gets the translation of the view.
    276      */
    277     public float getTranslation() {
    278         return getTranslationX();
    279     }
    280 
    281     public void onHeightReset() {
    282         if (mOnHeightChangedListener != null) {
    283             mOnHeightChangedListener.onReset(this);
    284         }
    285     }
    286 
    287     /**
    288      * This method returns the drawing rect for the view which is different from the regular
    289      * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
    290      * position 0 and usually the translation is neglected. Since we are manually clipping this
    291      * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
    292      * ensure that accessibility and focusing work correctly.
    293      *
    294      * @param outRect The (scrolled) drawing bounds of the view.
    295      */
    296     @Override
    297     public void getDrawingRect(Rect outRect) {
    298         super.getDrawingRect(outRect);
    299         outRect.left += getTranslationX();
    300         outRect.right += getTranslationX();
    301         outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
    302         outRect.top += getTranslationY() + getClipTopAmount();
    303     }
    304 
    305     @Override
    306     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
    307         super.getBoundsOnScreen(outRect, clipToParent);
    308         if (getTop() + getTranslationY() < 0) {
    309             // We got clipped to the parent here - make sure we undo that.
    310             outRect.top += getTop() + getTranslationY();
    311         }
    312         outRect.bottom = outRect.top + getActualHeight();
    313         outRect.top += getClipTopAmount();
    314     }
    315 
    316     public boolean isSummaryWithChildren() {
    317         return false;
    318     }
    319 
    320     public boolean areChildrenExpanded() {
    321         return false;
    322     }
    323 
    324     private void updateClipping() {
    325         if (mClipToActualHeight) {
    326             int top = getClipTopAmount();
    327             if (top >= getActualHeight()) {
    328                 top = getActualHeight() - 1;
    329             }
    330             mClipRect.set(0, top, getWidth(), getActualHeight() + getExtraBottomPadding());
    331             setClipBounds(mClipRect);
    332         } else {
    333             setClipBounds(null);
    334         }
    335     }
    336 
    337     public void setClipToActualHeight(boolean clipToActualHeight) {
    338         mClipToActualHeight = clipToActualHeight;
    339         updateClipping();
    340     }
    341 
    342     public boolean willBeGone() {
    343         return mWillBeGone;
    344     }
    345 
    346     public void setWillBeGone(boolean willBeGone) {
    347         mWillBeGone = willBeGone;
    348     }
    349 
    350     public int getMinClipTopAmount() {
    351         return mMinClipTopAmount;
    352     }
    353 
    354     public void setMinClipTopAmount(int minClipTopAmount) {
    355         mMinClipTopAmount = minClipTopAmount;
    356     }
    357 
    358     @Override
    359     public void setLayerType(int layerType, Paint paint) {
    360         if (hasOverlappingRendering()) {
    361             super.setLayerType(layerType, paint);
    362         }
    363     }
    364 
    365     @Override
    366     public boolean hasOverlappingRendering() {
    367         // Otherwise it will be clipped
    368         return super.hasOverlappingRendering() && getActualHeight() <= getHeight();
    369     }
    370 
    371     public float getShadowAlpha() {
    372         return 0.0f;
    373     }
    374 
    375     public void setShadowAlpha(float shadowAlpha) {
    376     }
    377 
    378     /**
    379      * @return an amount between 0 and 1 of increased padding that this child needs
    380      */
    381     public float getIncreasedPaddingAmount() {
    382         return 0.0f;
    383     }
    384 
    385     public boolean mustStayOnScreen() {
    386         return false;
    387     }
    388 
    389     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
    390             int outlineTranslation) {
    391     }
    392 
    393     public float getOutlineAlpha() {
    394         return 0.0f;
    395     }
    396 
    397     public int getOutlineTranslation() {
    398         return 0;
    399     }
    400 
    401     public void setChangingPosition(boolean changingPosition) {
    402         mChangingPosition = changingPosition;
    403     }
    404 
    405     public boolean isChangingPosition() {
    406         return mChangingPosition;
    407     }
    408 
    409     public void setTransientContainer(ViewGroup transientContainer) {
    410         mTransientContainer = transientContainer;
    411     }
    412 
    413     public ViewGroup getTransientContainer() {
    414         return mTransientContainer;
    415     }
    416 
    417     /**
    418      * @return padding used to alter how much of the view is clipped.
    419      */
    420     public int getExtraBottomPadding() {
    421         return 0;
    422     }
    423 
    424     /**
    425      * @return true if the group's expansion state is changing, false otherwise.
    426      */
    427     public boolean isGroupExpansionChanging() {
    428         return false;
    429     }
    430 
    431     public boolean isGroupExpanded() {
    432         return false;
    433     }
    434 
    435     public boolean isChildInGroup() {
    436         return false;
    437     }
    438 
    439     public void setActualHeightAnimating(boolean animating) {}
    440 
    441     /**
    442      * A listener notifying when {@link #getActualHeight} changes.
    443      */
    444     public interface OnHeightChangedListener {
    445 
    446         /**
    447          * @param view the view for which the height changed, or {@code null} if just the top
    448          *             padding or the padding between the elements changed
    449          * @param needsAnimation whether the view height needs to be animated
    450          */
    451         void onHeightChanged(ExpandableView view, boolean needsAnimation);
    452 
    453         /**
    454          * Called when the view is reset and therefore the height will change abruptly
    455          *
    456          * @param view The view which was reset.
    457          */
    458         void onReset(ExpandableView view);
    459     }
    460 }
    461