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.Rect;
     21 import android.util.AttributeSet;
     22 import android.view.MotionEvent;
     23 import android.view.View;
     24 import android.view.ViewGroup;
     25 import android.widget.FrameLayout;
     26 import com.android.systemui.R;
     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     private final int mBottomDecorHeight;
     37     protected OnHeightChangedListener mOnHeightChangedListener;
     38     protected int mMaxViewHeight;
     39     private int mActualHeight;
     40     protected int mClipTopAmount;
     41     private boolean mActualHeightInitialized;
     42     private boolean mDark;
     43     private ArrayList<View> mMatchParentViews = new ArrayList<View>();
     44     private int mClipTopOptimization;
     45     private static Rect mClipRect = new Rect();
     46     private boolean mWillBeGone;
     47     private int mMinClipTopAmount = 0;
     48 
     49     public ExpandableView(Context context, AttributeSet attrs) {
     50         super(context, attrs);
     51         mMaxViewHeight = getResources().getDimensionPixelSize(
     52                 R.dimen.notification_max_height);
     53         mBottomDecorHeight = resolveBottomDecorHeight();
     54     }
     55 
     56     protected int resolveBottomDecorHeight() {
     57         return getResources().getDimensionPixelSize(
     58                 R.dimen.notification_bottom_decor_height);
     59     }
     60 
     61     @Override
     62     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     63         int ownMaxHeight = mMaxViewHeight;
     64         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
     65         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
     66         if (hasFixedHeight) {
     67             // We have a height set in our layout, so we want to be at most as big as given
     68             ownMaxHeight = Math.min(MeasureSpec.getSize(heightMeasureSpec), ownMaxHeight);
     69         }
     70         int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
     71         int maxChildHeight = 0;
     72         int childCount = getChildCount();
     73         for (int i = 0; i < childCount; i++) {
     74             View child = getChildAt(i);
     75             if (child.getVisibility() == GONE || isChildInvisible(child)) {
     76                 continue;
     77             }
     78             int childHeightSpec = newHeightSpec;
     79             ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
     80             if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
     81                 if (layoutParams.height >= 0) {
     82                     // An actual height is set
     83                     childHeightSpec = layoutParams.height > ownMaxHeight
     84                         ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
     85                         : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
     86                 }
     87                 child.measure(
     88                         getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
     89                         childHeightSpec);
     90                 int childHeight = child.getMeasuredHeight();
     91                 maxChildHeight = Math.max(maxChildHeight, childHeight);
     92             } else {
     93                 mMatchParentViews.add(child);
     94             }
     95         }
     96         int ownHeight = hasFixedHeight ? ownMaxHeight : Math.min(ownMaxHeight, maxChildHeight);
     97         newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
     98         for (View child : mMatchParentViews) {
     99             child.measure(getChildMeasureSpec(
    100                     widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
    101                     newHeightSpec);
    102         }
    103         mMatchParentViews.clear();
    104         int width = MeasureSpec.getSize(widthMeasureSpec);
    105         if (canHaveBottomDecor()) {
    106             // We always account for the expandAction as well.
    107             ownHeight += mBottomDecorHeight;
    108         }
    109         setMeasuredDimension(width, ownHeight);
    110     }
    111 
    112     @Override
    113     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    114         super.onLayout(changed, left, top, right, bottom);
    115         if (!mActualHeightInitialized && mActualHeight == 0) {
    116             int initialHeight = getInitialHeight();
    117             if (initialHeight != 0) {
    118                 setContentHeight(initialHeight);
    119             }
    120         }
    121         updateClipping();
    122     }
    123 
    124     /**
    125      * Resets the height of the view on the next layout pass
    126      */
    127     protected void resetActualHeight() {
    128         mActualHeight = 0;
    129         mActualHeightInitialized = false;
    130         requestLayout();
    131     }
    132 
    133     protected int getInitialHeight() {
    134         return getHeight();
    135     }
    136 
    137     @Override
    138     public boolean dispatchGenericMotionEvent(MotionEvent ev) {
    139         if (filterMotionEvent(ev)) {
    140             return super.dispatchGenericMotionEvent(ev);
    141         }
    142         return false;
    143     }
    144 
    145     @Override
    146     public boolean dispatchTouchEvent(MotionEvent ev) {
    147         if (filterMotionEvent(ev)) {
    148             return super.dispatchTouchEvent(ev);
    149         }
    150         return false;
    151     }
    152 
    153     protected boolean filterMotionEvent(MotionEvent event) {
    154         return event.getActionMasked() != MotionEvent.ACTION_DOWN
    155                 && event.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER
    156                 && event.getActionMasked() != MotionEvent.ACTION_HOVER_MOVE
    157                 || event.getY() > mClipTopAmount && event.getY() < mActualHeight;
    158     }
    159 
    160     /**
    161      * Sets the actual height of this notification. This is different than the laid out
    162      * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
    163      *
    164      * @param actualHeight The height of this notification.
    165      * @param notifyListeners Whether the listener should be informed about the change.
    166      */
    167     public void setActualHeight(int actualHeight, boolean notifyListeners) {
    168         mActualHeightInitialized = true;
    169         mActualHeight = actualHeight;
    170         updateClipping();
    171         if (notifyListeners) {
    172             notifyHeightChanged(false  /* needsAnimation */);
    173         }
    174     }
    175 
    176     public void setContentHeight(int contentHeight) {
    177         setActualHeight(contentHeight + getBottomDecorHeight(), true);
    178     }
    179 
    180     /**
    181      * See {@link #setActualHeight}.
    182      *
    183      * @return The current actual height of this notification.
    184      */
    185     public int getActualHeight() {
    186         return mActualHeight;
    187     }
    188 
    189     /**
    190      * This view may have a bottom decor which will be placed below the content. If it has one, this
    191      * view will be layouted higher than just the content by {@link #mBottomDecorHeight}.
    192      * @return the height of the decor if it currently has one
    193      */
    194     public int getBottomDecorHeight() {
    195         return hasBottomDecor() ? mBottomDecorHeight : 0;
    196     }
    197 
    198     /**
    199      * @return whether this view may have a bottom decor at all. This will force the view to layout
    200      *         itself higher than just it's content
    201      */
    202     protected boolean canHaveBottomDecor() {
    203         return false;
    204     }
    205 
    206     /**
    207      * @return whether this view has a decor view below it's content. This will make the intrinsic
    208      *         height from {@link #getIntrinsicHeight()} higher as well
    209      */
    210     protected boolean hasBottomDecor() {
    211         return false;
    212     }
    213 
    214     /**
    215      * @return The maximum height of this notification.
    216      */
    217     public int getMaxContentHeight() {
    218         return getHeight();
    219     }
    220 
    221     /**
    222      * @return The minimum content height of this notification.
    223      */
    224     public int getMinHeight() {
    225         return getHeight();
    226     }
    227 
    228     /**
    229      * Sets the notification as dimmed. The default implementation does nothing.
    230      *
    231      * @param dimmed Whether the notification should be dimmed.
    232      * @param fade Whether an animation should be played to change the state.
    233      */
    234     public void setDimmed(boolean dimmed, boolean fade) {
    235     }
    236 
    237     /**
    238      * Sets the notification as dark. The default implementation does nothing.
    239      *
    240      * @param dark Whether the notification should be dark.
    241      * @param fade Whether an animation should be played to change the state.
    242      * @param delay If fading, the delay of the animation.
    243      */
    244     public void setDark(boolean dark, boolean fade, long delay) {
    245         mDark = dark;
    246     }
    247 
    248     public boolean isDark() {
    249         return mDark;
    250     }
    251 
    252     /**
    253      * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
    254      * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
    255      * of a stack scroller update such that the updated intrinsic height (which is dependent on
    256      * whether private or public layout is showing) gets taken into account into all layout
    257      * calculations.
    258      */
    259     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
    260     }
    261 
    262     /**
    263      * Sets whether the notification should hide its private contents if it is sensitive.
    264      */
    265     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
    266             long duration) {
    267     }
    268 
    269     /**
    270      * @return The desired notification height.
    271      */
    272     public int getIntrinsicHeight() {
    273         return getHeight();
    274     }
    275 
    276     /**
    277      * Sets the amount this view should be clipped from the top. This is used when an expanded
    278      * notification is scrolling in the top or bottom stack.
    279      *
    280      * @param clipTopAmount The amount of pixels this view should be clipped from top.
    281      */
    282     public void setClipTopAmount(int clipTopAmount) {
    283         mClipTopAmount = clipTopAmount;
    284     }
    285 
    286     public int getClipTopAmount() {
    287         return mClipTopAmount;
    288     }
    289 
    290     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
    291         mOnHeightChangedListener = listener;
    292     }
    293 
    294     /**
    295      * @return Whether we can expand this views content.
    296      */
    297     public boolean isContentExpandable() {
    298         return false;
    299     }
    300 
    301     public void notifyHeightChanged(boolean needsAnimation) {
    302         if (mOnHeightChangedListener != null) {
    303             mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
    304         }
    305     }
    306 
    307     public boolean isTransparent() {
    308         return false;
    309     }
    310 
    311     /**
    312      * Perform a remove animation on this view.
    313      *
    314      * @param duration The duration of the remove animation.
    315      * @param translationDirection The direction value from [-1 ... 1] indicating in which the
    316      *                             animation should be performed. A value of -1 means that The
    317      *                             remove animation should be performed upwards,
    318      *                             such that the  child appears to be going away to the top. 1
    319      *                             Should mean the opposite.
    320      * @param onFinishedRunnable A runnable which should be run when the animation is finished.
    321      */
    322     public abstract void performRemoveAnimation(long duration, float translationDirection,
    323             Runnable onFinishedRunnable);
    324 
    325     public abstract void performAddAnimation(long delay, long duration);
    326 
    327     public void setBelowSpeedBump(boolean below) {
    328     }
    329 
    330     public void onHeightReset() {
    331         if (mOnHeightChangedListener != null) {
    332             mOnHeightChangedListener.onReset(this);
    333         }
    334     }
    335 
    336     /**
    337      * This method returns the drawing rect for the view which is different from the regular
    338      * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
    339      * position 0 and usually the translation is neglected. Since we are manually clipping this
    340      * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
    341      * ensure that accessibility and focusing work correctly.
    342      *
    343      * @param outRect The (scrolled) drawing bounds of the view.
    344      */
    345     @Override
    346     public void getDrawingRect(Rect outRect) {
    347         super.getDrawingRect(outRect);
    348         outRect.left += getTranslationX();
    349         outRect.right += getTranslationX();
    350         outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
    351         outRect.top += getTranslationY() + getClipTopAmount();
    352     }
    353 
    354     @Override
    355     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
    356         super.getBoundsOnScreen(outRect, clipToParent);
    357         outRect.bottom = outRect.top + getActualHeight();
    358         outRect.top += getClipTopOptimization();
    359     }
    360 
    361     public int getContentHeight() {
    362         return mActualHeight - getBottomDecorHeight();
    363     }
    364 
    365     /**
    366      * @return whether the given child can be ignored for layouting and measuring purposes
    367      */
    368     protected boolean isChildInvisible(View child) {
    369         return false;
    370     }
    371 
    372     public boolean areChildrenExpanded() {
    373         return false;
    374     }
    375 
    376     private void updateClipping() {
    377         int top = mClipTopOptimization;
    378         if (top >= getActualHeight()) {
    379             top = getActualHeight() - 1;
    380         }
    381         mClipRect.set(0, top, getWidth(), getActualHeight());
    382         setClipBounds(mClipRect);
    383     }
    384 
    385     public int getClipTopOptimization() {
    386         return mClipTopOptimization;
    387     }
    388 
    389     /**
    390      * Set that the view will be clipped by a given amount from the top. Contrary to
    391      * {@link #setClipTopAmount} this amount doesn't effect shadows and the background.
    392      *
    393      * @param clipTopOptimization the amount to clip from the top
    394      */
    395     public void setClipTopOptimization(int clipTopOptimization) {
    396         mClipTopOptimization = clipTopOptimization;
    397         updateClipping();
    398     }
    399 
    400     public boolean willBeGone() {
    401         return mWillBeGone;
    402     }
    403 
    404     public void setWillBeGone(boolean willBeGone) {
    405         mWillBeGone = willBeGone;
    406     }
    407 
    408     public int getMinClipTopAmount() {
    409         return mMinClipTopAmount;
    410     }
    411 
    412     public void setMinClipTopAmount(int minClipTopAmount) {
    413         mMinClipTopAmount = minClipTopAmount;
    414     }
    415 
    416     /**
    417      * A listener notifying when {@link #getActualHeight} changes.
    418      */
    419     public interface OnHeightChangedListener {
    420 
    421         /**
    422          * @param view the view for which the height changed, or {@code null} if just the top
    423          *             padding or the padding between the elements changed
    424          * @param needsAnimation whether the view height needs to be animated
    425          */
    426         void onHeightChanged(ExpandableView view, boolean needsAnimation);
    427 
    428         /**
    429          * Called when the view is reset and therefore the height will change abruptly
    430          *
    431          * @param view The view which was reset.
    432          */
    433         void onReset(ExpandableView view);
    434     }
    435 }
    436