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