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 mMaxNotificationHeight;
     37 
     38     private OnHeightChangedListener mOnHeightChangedListener;
     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 
     45     public ExpandableView(Context context, AttributeSet attrs) {
     46         super(context, attrs);
     47         mMaxNotificationHeight = getResources().getDimensionPixelSize(
     48                 R.dimen.notification_max_height);
     49     }
     50 
     51     @Override
     52     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     53         int ownMaxHeight = mMaxNotificationHeight;
     54         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
     55         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
     56         boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
     57         if (hasFixedHeight || isHeightLimited) {
     58             int size = MeasureSpec.getSize(heightMeasureSpec);
     59             ownMaxHeight = Math.min(ownMaxHeight, size);
     60         }
     61         int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
     62         int maxChildHeight = 0;
     63         int childCount = getChildCount();
     64         for (int i = 0; i < childCount; i++) {
     65             View child = getChildAt(i);
     66             int childHeightSpec = newHeightSpec;
     67             ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
     68             if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
     69                 if (layoutParams.height >= 0) {
     70                     // An actual height is set
     71                     childHeightSpec = layoutParams.height > ownMaxHeight
     72                         ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
     73                         : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
     74                 }
     75                 child.measure(
     76                         getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
     77                         childHeightSpec);
     78                 int childHeight = child.getMeasuredHeight();
     79                 maxChildHeight = Math.max(maxChildHeight, childHeight);
     80             } else {
     81                 mMatchParentViews.add(child);
     82             }
     83         }
     84         int ownHeight = hasFixedHeight ? ownMaxHeight : maxChildHeight;
     85         newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
     86         for (View child : mMatchParentViews) {
     87             child.measure(getChildMeasureSpec(
     88                     widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
     89                     newHeightSpec);
     90         }
     91         mMatchParentViews.clear();
     92         int width = MeasureSpec.getSize(widthMeasureSpec);
     93         setMeasuredDimension(width, ownHeight);
     94     }
     95 
     96     @Override
     97     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
     98         super.onLayout(changed, left, top, right, bottom);
     99         if (!mActualHeightInitialized && mActualHeight == 0) {
    100             int initialHeight = getInitialHeight();
    101             if (initialHeight != 0) {
    102                 setActualHeight(initialHeight);
    103             }
    104         }
    105     }
    106 
    107     /**
    108      * Resets the height of the view on the next layout pass
    109      */
    110     protected void resetActualHeight() {
    111         mActualHeight = 0;
    112         mActualHeightInitialized = false;
    113         requestLayout();
    114     }
    115 
    116     protected int getInitialHeight() {
    117         return getHeight();
    118     }
    119 
    120     @Override
    121     public boolean dispatchTouchEvent(MotionEvent ev) {
    122         if (filterMotionEvent(ev)) {
    123             return super.dispatchTouchEvent(ev);
    124         }
    125         return false;
    126     }
    127 
    128     protected boolean filterMotionEvent(MotionEvent event) {
    129         return event.getActionMasked() != MotionEvent.ACTION_DOWN
    130                 || event.getY() > mClipTopAmount && event.getY() < mActualHeight;
    131     }
    132 
    133     /**
    134      * Sets the actual height of this notification. This is different than the laid out
    135      * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
    136      *
    137      * @param actualHeight The height of this notification.
    138      * @param notifyListeners Whether the listener should be informed about the change.
    139      */
    140     public void setActualHeight(int actualHeight, boolean notifyListeners) {
    141         mActualHeightInitialized = true;
    142         mActualHeight = actualHeight;
    143         if (notifyListeners) {
    144             notifyHeightChanged();
    145         }
    146     }
    147 
    148     public void setActualHeight(int actualHeight) {
    149         setActualHeight(actualHeight, true);
    150     }
    151 
    152     /**
    153      * See {@link #setActualHeight}.
    154      *
    155      * @return The current actual height of this notification.
    156      */
    157     public int getActualHeight() {
    158         return mActualHeight;
    159     }
    160 
    161     /**
    162      * @return The maximum height of this notification.
    163      */
    164     public int getMaxHeight() {
    165         return getHeight();
    166     }
    167 
    168     /**
    169      * @return The minimum height of this notification.
    170      */
    171     public int getMinHeight() {
    172         return getHeight();
    173     }
    174 
    175     /**
    176      * Sets the notification as dimmed. The default implementation does nothing.
    177      *
    178      * @param dimmed Whether the notification should be dimmed.
    179      * @param fade Whether an animation should be played to change the state.
    180      */
    181     public void setDimmed(boolean dimmed, boolean fade) {
    182     }
    183 
    184     /**
    185      * Sets the notification as dark. The default implementation does nothing.
    186      *
    187      * @param dark Whether the notification should be dark.
    188      * @param fade Whether an animation should be played to change the state.
    189      * @param delay If fading, the delay of the animation.
    190      */
    191     public void setDark(boolean dark, boolean fade, long delay) {
    192         mDark = dark;
    193     }
    194 
    195     public boolean isDark() {
    196         return mDark;
    197     }
    198 
    199     /**
    200      * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
    201      * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
    202      * of a stack scroller update such that the updated intrinsic height (which is dependent on
    203      * whether private or public layout is showing) gets taken into account into all layout
    204      * calculations.
    205      */
    206     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
    207     }
    208 
    209     /**
    210      * Sets whether the notification should hide its private contents if it is sensitive.
    211      */
    212     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
    213             long duration) {
    214     }
    215 
    216     /**
    217      * @return The desired notification height.
    218      */
    219     public int getIntrinsicHeight() {
    220         return getHeight();
    221     }
    222 
    223     /**
    224      * Sets the amount this view should be clipped from the top. This is used when an expanded
    225      * notification is scrolling in the top or bottom stack.
    226      *
    227      * @param clipTopAmount The amount of pixels this view should be clipped from top.
    228      */
    229     public void setClipTopAmount(int clipTopAmount) {
    230         mClipTopAmount = clipTopAmount;
    231     }
    232 
    233     public int getClipTopAmount() {
    234         return mClipTopAmount;
    235     }
    236 
    237     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
    238         mOnHeightChangedListener = listener;
    239     }
    240 
    241     /**
    242      * @return Whether we can expand this views content.
    243      */
    244     public boolean isContentExpandable() {
    245         return false;
    246     }
    247 
    248     public void notifyHeightChanged() {
    249         if (mOnHeightChangedListener != null) {
    250             mOnHeightChangedListener.onHeightChanged(this);
    251         }
    252     }
    253 
    254     public boolean isTransparent() {
    255         return false;
    256     }
    257 
    258     /**
    259      * Perform a remove animation on this view.
    260      *
    261      * @param duration The duration of the remove animation.
    262      * @param translationDirection The direction value from [-1 ... 1] indicating in which the
    263      *                             animation should be performed. A value of -1 means that The
    264      *                             remove animation should be performed upwards,
    265      *                             such that the  child appears to be going away to the top. 1
    266      *                             Should mean the opposite.
    267      * @param onFinishedRunnable A runnable which should be run when the animation is finished.
    268      */
    269     public abstract void performRemoveAnimation(long duration, float translationDirection,
    270             Runnable onFinishedRunnable);
    271 
    272     public abstract void performAddAnimation(long delay, long duration);
    273 
    274     public void setBelowSpeedBump(boolean below) {
    275     }
    276 
    277     public void onHeightReset() {
    278         if (mOnHeightChangedListener != null) {
    279             mOnHeightChangedListener.onReset(this);
    280         }
    281     }
    282 
    283     /**
    284      * This method returns the drawing rect for the view which is different from the regular
    285      * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
    286      * position 0 and usually the translation is neglected. Since we are manually clipping this
    287      * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
    288      * ensure that accessibility and focusing work correctly.
    289      *
    290      * @param outRect The (scrolled) drawing bounds of the view.
    291      */
    292     @Override
    293     public void getDrawingRect(Rect outRect) {
    294         super.getDrawingRect(outRect);
    295         outRect.left += getTranslationX();
    296         outRect.right += getTranslationX();
    297         outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
    298         outRect.top += getTranslationY() + getClipTopAmount();
    299     }
    300 
    301     /**
    302      * A listener notifying when {@link #getActualHeight} changes.
    303      */
    304     public interface OnHeightChangedListener {
    305 
    306         /**
    307          * @param view the view for which the height changed, or {@code null} if just the top
    308          *             padding or the padding between the elements changed
    309          */
    310         void onHeightChanged(ExpandableView view);
    311 
    312         /**
    313          * Called when the view is reset and therefore the height will change abruptly
    314          *
    315          * @param view The view which was reset.
    316          */
    317         void onReset(ExpandableView view);
    318     }
    319 }
    320