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 */ onGearTouched(ExpandableNotificationRow row, int x, int y)39 public void onGearTouched(ExpandableNotificationRow row, int x, int y); 40 41 /** 42 * Called when a notification is slid back over the gear. 43 */ onSettingsIconRowReset(ExpandableNotificationRow row)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 NotificationSettingsIconRow(Context context)64 public NotificationSettingsIconRow(Context context) { 65 this(context, null); 66 } 67 NotificationSettingsIconRow(Context context, AttributeSet attrs)68 public NotificationSettingsIconRow(Context context, AttributeSet attrs) { 69 this(context, attrs, 0); 70 } 71 NotificationSettingsIconRow(Context context, AttributeSet attrs, int defStyleAttr)72 public NotificationSettingsIconRow(Context context, AttributeSet attrs, int defStyleAttr) { 73 this(context, attrs, defStyleAttr, 0); 74 } 75 NotificationSettingsIconRow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)76 public NotificationSettingsIconRow(Context context, AttributeSet attrs, int defStyleAttr, 77 int defStyleRes) { 78 super(context, attrs); 79 } 80 81 @Override onFinishInflate()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 resetState()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 setGearListener(SettingsIconRowListener listener)106 public void setGearListener(SettingsIconRowListener listener) { 107 mListener = listener; 108 } 109 setNotificationRowParent(ExpandableNotificationRow parent)110 public void setNotificationRowParent(ExpandableNotificationRow parent) { 111 mParent = parent; 112 setIconLocation(mOnLeft); 113 } 114 setAppName(String appName)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 getNotificationParent()122 public ExpandableNotificationRow getNotificationParent() { 123 return mParent; 124 } 125 setGearAlpha(float alpha)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 */ isIconOnLeft()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 */ getSpaceForGear()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 */ isVisible()154 public boolean isVisible() { 155 return mGearIcon.getAlpha() > 0; 156 } 157 cancelFadeAnimator()158 public void cancelFadeAnimator() { 159 if (mFadeAnimator != null) { 160 mFadeAnimator.cancel(); 161 } 162 } 163 updateSettingsIcons(final float transX, final float size)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 fadeInSettings(final boolean fromLeft, final float transX, final float notiThreshold)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 updateVerticalLocation()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 onRtlPropertiesChanged(int layoutDirection)242 public void onRtlPropertiesChanged(int layoutDirection) { 243 setIconLocation(mOnLeft); 244 } 245 setIconLocation(boolean onLeft)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 isIconLocationChange(float translation)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