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 import com.android.systemui.statusbar.stack.ExpandableViewState;
     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.
    155      */
    156     public int getMinHeight() {
    157         return getHeight();
    158     }
    159 
    160     /**
    161      * @return The collapsed height of this view. Note that this might be different
    162      * than {@link #getMinHeight()} because some elements like groups may have different sizes when
    163      * they are system expanded.
    164      */
    165     public int getCollapsedHeight() {
    166         return getHeight();
    167     }
    168 
    169     /**
    170      * Sets the notification as dimmed. The default implementation does nothing.
    171      *
    172      * @param dimmed Whether the notification should be dimmed.
    173      * @param fade Whether an animation should be played to change the state.
    174      */
    175     public void setDimmed(boolean dimmed, boolean fade) {
    176     }
    177 
    178     /**
    179      * Sets the notification as dark. The default implementation does nothing.
    180      *
    181      * @param dark Whether the notification should be dark.
    182      * @param fade Whether an animation should be played to change the state.
    183      * @param delay If fading, the delay of the animation.
    184      */
    185     public void setDark(boolean dark, boolean fade, long delay) {
    186         mDark = dark;
    187     }
    188 
    189     public boolean isDark() {
    190         return mDark;
    191     }
    192 
    193     /**
    194      * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
    195      * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
    196      * of a stack scroller update such that the updated intrinsic height (which is dependent on
    197      * whether private or public layout is showing) gets taken into account into all layout
    198      * calculations.
    199      */
    200     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
    201     }
    202 
    203     /**
    204      * Sets whether the notification should hide its private contents if it is sensitive.
    205      */
    206     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
    207             long duration) {
    208     }
    209 
    210     /**
    211      * @return The desired notification height.
    212      */
    213     public int getIntrinsicHeight() {
    214         return getHeight();
    215     }
    216 
    217     /**
    218      * Sets the amount this view should be clipped from the top. This is used when an expanded
    219      * notification is scrolling in the top or bottom stack.
    220      *
    221      * @param clipTopAmount The amount of pixels this view should be clipped from top.
    222      */
    223     public void setClipTopAmount(int clipTopAmount) {
    224         mClipTopAmount = clipTopAmount;
    225         updateClipping();
    226     }
    227 
    228     /**
    229      * Set the amount the the notification is clipped on the bottom in addition to the regular
    230      * clipping. This is mainly used to clip something in a non-animated way without changing the
    231      * actual height of the notification and is purely visual.
    232      *
    233      * @param clipBottomAmount the amount to clip.
    234      */
    235     public void setClipBottomAmount(int clipBottomAmount) {
    236         mClipBottomAmount = clipBottomAmount;
    237         updateClipping();
    238     }
    239 
    240     public int getClipTopAmount() {
    241         return mClipTopAmount;
    242     }
    243 
    244     public int getClipBottomAmount() {
    245         return mClipBottomAmount;
    246     }
    247 
    248     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
    249         mOnHeightChangedListener = listener;
    250     }
    251 
    252     /**
    253      * @return Whether we can expand this views content.
    254      */
    255     public boolean isContentExpandable() {
    256         return false;
    257     }
    258 
    259     public void notifyHeightChanged(boolean needsAnimation) {
    260         if (mOnHeightChangedListener != null) {
    261             mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
    262         }
    263     }
    264 
    265     public boolean isTransparent() {
    266         return false;
    267     }
    268 
    269     /**
    270      * Perform a remove animation on this view.
    271      *
    272      * @param duration The duration of the remove animation.
    273      * @param translationDirection The direction value from [-1 ... 1] indicating in which the
    274      *                             animation should be performed. A value of -1 means that The
    275      *                             remove animation should be performed upwards,
    276      *                             such that the  child appears to be going away to the top. 1
    277      *                             Should mean the opposite.
    278      * @param onFinishedRunnable A runnable which should be run when the animation is finished.
    279      */
    280     public abstract void performRemoveAnimation(long duration, float translationDirection,
    281             Runnable onFinishedRunnable);
    282 
    283     public abstract void performAddAnimation(long delay, long duration);
    284 
    285     /**
    286      * Set the notification appearance to be below the speed bump.
    287      * @param below true if it is below.
    288      */
    289     public void setBelowSpeedBump(boolean below) {
    290     }
    291 
    292     public int getPinnedHeadsUpHeight() {
    293         return getIntrinsicHeight();
    294     }
    295 
    296 
    297     /**
    298      * Sets the translation of the view.
    299      */
    300     public void setTranslation(float translation) {
    301         setTranslationX(translation);
    302     }
    303 
    304     /**
    305      * Gets the translation of the view.
    306      */
    307     public float getTranslation() {
    308         return getTranslationX();
    309     }
    310 
    311     public void onHeightReset() {
    312         if (mOnHeightChangedListener != null) {
    313             mOnHeightChangedListener.onReset(this);
    314         }
    315     }
    316 
    317     /**
    318      * This method returns the drawing rect for the view which is different from the regular
    319      * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
    320      * position 0 and usually the translation is neglected. Since we are manually clipping this
    321      * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
    322      * ensure that accessibility and focusing work correctly.
    323      *
    324      * @param outRect The (scrolled) drawing bounds of the view.
    325      */
    326     @Override
    327     public void getDrawingRect(Rect outRect) {
    328         super.getDrawingRect(outRect);
    329         outRect.left += getTranslationX();
    330         outRect.right += getTranslationX();
    331         outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
    332         outRect.top += getTranslationY() + getClipTopAmount();
    333     }
    334 
    335     @Override
    336     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
    337         super.getBoundsOnScreen(outRect, clipToParent);
    338         if (getTop() + getTranslationY() < 0) {
    339             // We got clipped to the parent here - make sure we undo that.
    340             outRect.top += getTop() + getTranslationY();
    341         }
    342         outRect.bottom = outRect.top + getActualHeight();
    343         outRect.top += getClipTopAmount();
    344     }
    345 
    346     public boolean isSummaryWithChildren() {
    347         return false;
    348     }
    349 
    350     public boolean areChildrenExpanded() {
    351         return false;
    352     }
    353 
    354     private void updateClipping() {
    355         if (mClipToActualHeight) {
    356             int top = getClipTopAmount();
    357             mClipRect.set(0, top, getWidth(), Math.max(getActualHeight() + getExtraBottomPadding()
    358                                 - mClipBottomAmount, top));
    359             setClipBounds(mClipRect);
    360         } else {
    361             setClipBounds(null);
    362         }
    363     }
    364 
    365     public void setClipToActualHeight(boolean clipToActualHeight) {
    366         mClipToActualHeight = clipToActualHeight;
    367         updateClipping();
    368     }
    369 
    370     public boolean willBeGone() {
    371         return mWillBeGone;
    372     }
    373 
    374     public void setWillBeGone(boolean willBeGone) {
    375         mWillBeGone = willBeGone;
    376     }
    377 
    378     public int getMinClipTopAmount() {
    379         return mMinClipTopAmount;
    380     }
    381 
    382     public void setMinClipTopAmount(int minClipTopAmount) {
    383         mMinClipTopAmount = minClipTopAmount;
    384     }
    385 
    386     @Override
    387     public void setLayerType(int layerType, Paint paint) {
    388         if (hasOverlappingRendering()) {
    389             super.setLayerType(layerType, paint);
    390         }
    391     }
    392 
    393     @Override
    394     public boolean hasOverlappingRendering() {
    395         // Otherwise it will be clipped
    396         return super.hasOverlappingRendering() && getActualHeight() <= getHeight();
    397     }
    398 
    399     public float getShadowAlpha() {
    400         return 0.0f;
    401     }
    402 
    403     public void setShadowAlpha(float shadowAlpha) {
    404     }
    405 
    406     /**
    407      * @return an amount between -1 and 1 of increased padding that this child needs. 1 means it
    408      * needs a full increased padding while -1 means it needs no padding at all. For 0.0f the normal
    409      * padding is applied.
    410      */
    411     public float getIncreasedPaddingAmount() {
    412         return 0.0f;
    413     }
    414 
    415     public boolean mustStayOnScreen() {
    416         return false;
    417     }
    418 
    419     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
    420             int outlineTranslation) {
    421     }
    422 
    423     public float getOutlineAlpha() {
    424         return 0.0f;
    425     }
    426 
    427     public int getOutlineTranslation() {
    428         return 0;
    429     }
    430 
    431     public void setChangingPosition(boolean changingPosition) {
    432         mChangingPosition = changingPosition;
    433     }
    434 
    435     public boolean isChangingPosition() {
    436         return mChangingPosition;
    437     }
    438 
    439     public void setTransientContainer(ViewGroup transientContainer) {
    440         mTransientContainer = transientContainer;
    441     }
    442 
    443     public ViewGroup getTransientContainer() {
    444         return mTransientContainer;
    445     }
    446 
    447     /**
    448      * @return padding used to alter how much of the view is clipped.
    449      */
    450     public int getExtraBottomPadding() {
    451         return 0;
    452     }
    453 
    454     /**
    455      * @return true if the group's expansion state is changing, false otherwise.
    456      */
    457     public boolean isGroupExpansionChanging() {
    458         return false;
    459     }
    460 
    461     public boolean isGroupExpanded() {
    462         return false;
    463     }
    464 
    465     public boolean isChildInGroup() {
    466         return false;
    467     }
    468 
    469     public void setActualHeightAnimating(boolean animating) {}
    470 
    471     public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
    472         return new ExpandableViewState();
    473     }
    474 
    475     /**
    476      * @return whether the current view doesn't add height to the overall content. This means that
    477      * if it is added to a list of items, it's content will still have the same height.
    478      * An example is the notification shelf, that is always placed on top of another view.
    479      */
    480     public boolean hasNoContentHeight() {
    481         return false;
    482     }
    483 
    484     /**
    485      * @param inShelf whether the view is currently fully in the notification shelf.
    486      */
    487     public void setInShelf(boolean inShelf) {
    488         mInShelf = inShelf;
    489     }
    490 
    491     public boolean isInShelf() {
    492         return mInShelf;
    493     }
    494 
    495     /**
    496      * @param transformingInShelf whether the view is currently transforming into the shelf in an
    497      *                            animated way
    498      */
    499     public void setTransformingInShelf(boolean transformingInShelf) {
    500         mTransformingInShelf = transformingInShelf;
    501     }
    502 
    503     public boolean isTransformingIntoShelf() {
    504         return mTransformingInShelf;
    505     }
    506 
    507     public boolean isAboveShelf() {
    508         return false;
    509     }
    510 
    511     /**
    512      * A listener notifying when {@link #getActualHeight} changes.
    513      */
    514     public interface OnHeightChangedListener {
    515 
    516         /**
    517          * @param view the view for which the height changed, or {@code null} if just the top
    518          *             padding or the padding between the elements changed
    519          * @param needsAnimation whether the view height needs to be animated
    520          */
    521         void onHeightChanged(ExpandableView view, boolean needsAnimation);
    522 
    523         /**
    524          * Called when the view is reset and therefore the height will change abruptly
    525          *
    526          * @param view The view which was reset.
    527          */
    528         void onReset(ExpandableView view);
    529     }
    530 }
    531