Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2016 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.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ValueAnimator;
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.util.AttributeSet;
     25 import android.view.View;
     26 import android.widget.FrameLayout;
     27 
     28 import com.android.systemui.Interpolators;
     29 import com.android.systemui.R;
     30 
     31 public class NotificationSettingsIconRow extends FrameLayout implements View.OnClickListener {
     32 
     33     private static final int GEAR_ALPHA_ANIM_DURATION = 200;
     34 
     35     public interface SettingsIconRowListener {
     36         /**
     37          * Called when the gear behind a notification is touched.
     38          */
     39         public void onGearTouched(ExpandableNotificationRow row, int x, int y);
     40 
     41         /**
     42          * Called when a notification is slid back over the gear.
     43          */
     44         public void onSettingsIconRowReset(ExpandableNotificationRow row);
     45     }
     46 
     47     private ExpandableNotificationRow mParent;
     48     private AlphaOptimizedImageView mGearIcon;
     49     private float mHorizSpaceForGear;
     50     private SettingsIconRowListener mListener;
     51 
     52     private ValueAnimator mFadeAnimator;
     53     private boolean mSettingsFadedIn = false;
     54     private boolean mAnimating = false;
     55     private boolean mOnLeft = true;
     56     private boolean mDismissing = false;
     57     private boolean mSnapping = false;
     58     private boolean mIconPlaced = false;
     59 
     60     private int[] mGearLocation = new int[2];
     61     private int[] mParentLocation = new int[2];
     62     private int mVertSpaceForGear;
     63 
     64     public NotificationSettingsIconRow(Context context) {
     65         this(context, null);
     66     }
     67 
     68     public NotificationSettingsIconRow(Context context, AttributeSet attrs) {
     69         this(context, attrs, 0);
     70     }
     71 
     72     public NotificationSettingsIconRow(Context context, AttributeSet attrs, int defStyleAttr) {
     73         this(context, attrs, defStyleAttr, 0);
     74     }
     75 
     76     public NotificationSettingsIconRow(Context context, AttributeSet attrs, int defStyleAttr,
     77             int defStyleRes) {
     78         super(context, attrs);
     79     }
     80 
     81     @Override
     82     protected void onFinishInflate() {
     83         super.onFinishInflate();
     84         mGearIcon = (AlphaOptimizedImageView) findViewById(R.id.gear_icon);
     85         mGearIcon.setOnClickListener(this);
     86         setOnClickListener(this);
     87         mHorizSpaceForGear =
     88                 getResources().getDimensionPixelOffset(R.dimen.notification_gear_width);
     89         mVertSpaceForGear = getResources().getDimensionPixelOffset(R.dimen.notification_min_height);
     90         resetState();
     91     }
     92 
     93     public void resetState() {
     94         setGearAlpha(0f);
     95         mIconPlaced = false;
     96         mSettingsFadedIn = false;
     97         mAnimating = false;
     98         mSnapping = false;
     99         mDismissing = false;
    100         setIconLocation(true /* on left */);
    101         if (mListener != null) {
    102             mListener.onSettingsIconRowReset(mParent);
    103         }
    104     }
    105 
    106     public void setGearListener(SettingsIconRowListener listener) {
    107         mListener = listener;
    108     }
    109 
    110     public void setNotificationRowParent(ExpandableNotificationRow parent) {
    111         mParent = parent;
    112         setIconLocation(mOnLeft);
    113     }
    114 
    115     public void setAppName(String appName) {
    116         Resources res = getResources();
    117         String description = String.format(res.getString(R.string.notification_gear_accessibility),
    118                 appName);
    119         mGearIcon.setContentDescription(description);
    120     }
    121 
    122     public ExpandableNotificationRow getNotificationParent() {
    123         return mParent;
    124     }
    125 
    126     public void setGearAlpha(float alpha) {
    127         if (alpha == 0) {
    128             mSettingsFadedIn = false; // Can fade in again once it's gone.
    129             setVisibility(View.INVISIBLE);
    130         } else {
    131             setVisibility(View.VISIBLE);
    132         }
    133         mGearIcon.setAlpha(alpha);
    134     }
    135 
    136     /**
    137      * Returns whether the icon is on the left side of the view or not.
    138      */
    139     public boolean isIconOnLeft() {
    140         return mOnLeft;
    141     }
    142 
    143     /**
    144      * Returns the horizontal space in pixels required to display the gear behind a notification.
    145      */
    146     public float getSpaceForGear() {
    147         return mHorizSpaceForGear;
    148     }
    149 
    150     /**
    151      * Indicates whether the gear is visible at 1 alpha. Does not indicate
    152      * if entire view is visible.
    153      */
    154     public boolean isVisible() {
    155         return mGearIcon.getAlpha() > 0;
    156     }
    157 
    158     public void cancelFadeAnimator() {
    159         if (mFadeAnimator != null) {
    160             mFadeAnimator.cancel();
    161         }
    162     }
    163 
    164     public void updateSettingsIcons(final float transX, final float size) {
    165         if (mAnimating || !mSettingsFadedIn) {
    166             // Don't adjust when animating, or if the gear hasn't been shown yet.
    167             return;
    168         }
    169 
    170         final float fadeThreshold = size * 0.3f;
    171         final float absTrans = Math.abs(transX);
    172         float desiredAlpha = 0;
    173 
    174         if (absTrans == 0) {
    175             desiredAlpha = 0;
    176         } else if (absTrans <= fadeThreshold) {
    177             desiredAlpha = 1;
    178         } else {
    179             desiredAlpha = 1 - ((absTrans - fadeThreshold) / (size - fadeThreshold));
    180         }
    181         setGearAlpha(desiredAlpha);
    182     }
    183 
    184     public void fadeInSettings(final boolean fromLeft, final float transX,
    185             final float notiThreshold) {
    186         if (mDismissing || mAnimating) {
    187             return;
    188         }
    189         if (isIconLocationChange(transX)) {
    190             setGearAlpha(0f);
    191         }
    192         setIconLocation(transX > 0 /* fromLeft */);
    193         mFadeAnimator = ValueAnimator.ofFloat(mGearIcon.getAlpha(), 1);
    194         mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    195             @Override
    196             public void onAnimationUpdate(ValueAnimator animation) {
    197                 final float absTrans = Math.abs(transX);
    198 
    199                 boolean pastGear = (fromLeft && transX <= notiThreshold)
    200                         || (!fromLeft && absTrans <= notiThreshold);
    201                 if (pastGear && !mSettingsFadedIn) {
    202                     setGearAlpha((float) animation.getAnimatedValue());
    203                 }
    204             }
    205         });
    206         mFadeAnimator.addListener(new AnimatorListenerAdapter() {
    207             @Override
    208             public void onAnimationStart(Animator animation) {
    209                 mAnimating = true;
    210             }
    211 
    212             @Override
    213             public void onAnimationCancel(Animator animation) {
    214                 // TODO should animate back to 0f from current alpha
    215                 mGearIcon.setAlpha(0f);
    216             }
    217 
    218             @Override
    219             public void onAnimationEnd(Animator animation) {
    220                 mAnimating = false;
    221                 mSettingsFadedIn = mGearIcon.getAlpha() == 1;
    222             }
    223         });
    224         mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
    225         mFadeAnimator.setDuration(GEAR_ALPHA_ANIM_DURATION);
    226         mFadeAnimator.start();
    227     }
    228 
    229     public void updateVerticalLocation() {
    230         if (mParent == null) {
    231             return;
    232         }
    233         int parentHeight = mParent.getCollapsedHeight();
    234         if (parentHeight < mVertSpaceForGear) {
    235             mGearIcon.setTranslationY((parentHeight / 2) - (mGearIcon.getHeight() / 2));
    236         } else {
    237             mGearIcon.setTranslationY((mVertSpaceForGear - mGearIcon.getHeight()) / 2);
    238         }
    239     }
    240 
    241     @Override
    242     public void onRtlPropertiesChanged(int layoutDirection) {
    243         setIconLocation(mOnLeft);
    244     }
    245 
    246     public void setIconLocation(boolean onLeft) {
    247         if ((mIconPlaced && onLeft == mOnLeft) || mSnapping || mParent == null
    248                 || mGearIcon.getWidth() == 0) {
    249             // Do nothing
    250             return;
    251         }
    252         final boolean isRtl = mParent.isLayoutRtl();
    253 
    254         // TODO No need to cast to float here once b/28050538 is fixed.
    255         final float left = (float) (isRtl ? -(mParent.getWidth() - mHorizSpaceForGear) : 0);
    256         final float right = (float) (isRtl ? 0 : (mParent.getWidth() - mHorizSpaceForGear));
    257         final float centerX = ((mHorizSpaceForGear - mGearIcon.getWidth()) / 2);
    258         setTranslationX(onLeft ? left + centerX : right + centerX);
    259         mOnLeft = onLeft;
    260         mIconPlaced = true;
    261     }
    262 
    263     public boolean isIconLocationChange(float translation) {
    264         boolean onLeft = translation > mGearIcon.getPaddingStart();
    265         boolean onRight = translation < -mGearIcon.getPaddingStart();
    266         if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) {
    267             return true;
    268         }
    269         return false;
    270     }
    271 
    272     public void setDismissing() {
    273         mDismissing = true;
    274     }
    275 
    276     public void setSnapping(boolean snapping) {
    277         mSnapping = snapping;
    278     }
    279 
    280     @Override
    281     public void onClick(View v) {
    282         if (v.getId() == R.id.gear_icon) {
    283             if (mListener != null) {
    284                 mGearIcon.getLocationOnScreen(mGearLocation);
    285                 mParent.getLocationOnScreen(mParentLocation);
    286 
    287                 final int centerX = (int) (mHorizSpaceForGear / 2);
    288                 final int centerY =
    289                         (int) (mGearIcon.getTranslationY() * 2 + mGearIcon.getHeight())/ 2;
    290                 final int x = mGearLocation[0] - mParentLocation[0] + centerX;
    291                 final int y = mGearLocation[1] - mParentLocation[1] + centerY;
    292                 mListener.onGearTouched(mParent, x, y);
    293             }
    294         } else {
    295             // Do nothing when the background is touched.
    296         }
    297     }
    298 }
    299