NotificationMenuRow.java revision 5bec68fbea1a02d7debc2eeeaf01b9478de216e5
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 17package com.android.systemui.statusbar; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ValueAnimator; 22import android.content.Context; 23import android.content.res.Resources; 24import android.graphics.drawable.Drawable; 25import android.util.AttributeSet; 26import android.util.Log; 27import android.view.LayoutInflater; 28import android.view.View; 29import android.widget.FrameLayout; 30 31import java.util.ArrayList; 32 33import com.android.systemui.Dependency; 34import com.android.systemui.Interpolators; 35import com.android.systemui.R; 36import com.android.systemui.plugins.PluginListener; 37import com.android.systemui.plugins.PluginManager; 38import com.android.systemui.plugins.statusbar.NotificationMenuRowProvider; 39import com.android.systemui.plugins.statusbar.NotificationMenuRowProvider.MenuItem; 40import com.android.systemui.plugins.statusbar.NotificationMenuRowProvider.OnMenuClickListener; 41 42public class NotificationMenuRow extends FrameLayout 43 implements PluginListener<NotificationMenuRowProvider>, View.OnClickListener { 44 45 private static final int ICON_ALPHA_ANIM_DURATION = 200; 46 47 private ExpandableNotificationRow mParent; 48 private OnMenuClickListener mListener; 49 private NotificationMenuRowProvider mMenuProvider; 50 private ArrayList<MenuItem> mMenuItems = new ArrayList<>(); 51 52 private ValueAnimator mFadeAnimator; 53 private boolean mMenuFadedIn = false; 54 private boolean mAnimating = false; 55 private boolean mOnLeft = true; 56 private boolean mDismissing = false; 57 private boolean mSnapping = false; 58 private boolean mIconsPlaced = false; 59 60 private int[] mIconLocation = new int[2]; 61 private int[] mParentLocation = new int[2]; 62 63 private float mHorizSpaceForIcon; 64 private int mVertSpaceForIcons; 65 66 private int mIconPadding; 67 private int mIconTint; 68 69 private float mAlpha = 0f; 70 71 public NotificationMenuRow(Context context) { 72 this(context, null); 73 } 74 75 public NotificationMenuRow(Context context, AttributeSet attrs) { 76 this(context, attrs, 0); 77 } 78 79 public NotificationMenuRow(Context context, AttributeSet attrs, int defStyleAttr) { 80 this(context, attrs, defStyleAttr, 0); 81 } 82 83 public NotificationMenuRow(Context context, AttributeSet attrs, int defStyleAttr, 84 int defStyleRes) { 85 super(context, attrs); 86 mMenuItems.addAll(getDefaultNotificationMenuItems()); 87 } 88 89 @Override 90 protected void onAttachedToWindow() { 91 super.onAttachedToWindow(); 92 Dependency.get(PluginManager.class).addPluginListener( 93 this, NotificationMenuRowProvider.class, false /* Allow multiple */); 94 } 95 96 @Override 97 protected void onDetachedFromWindow() { 98 super.onDetachedFromWindow(); 99 Dependency.get(PluginManager.class).removePluginListener(this); 100 } 101 102 @Override 103 protected void onFinishInflate() { 104 super.onFinishInflate(); 105 final Resources res = getResources(); 106 mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size); 107 mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height); 108 mIconPadding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding); 109 mIconTint = res.getColor(R.color.notification_gear_color); 110 updateMenu(false /* notify */); 111 } 112 113 public static MenuItem getLongpressMenuItem(Context context) { 114 Resources res = context.getResources(); 115 Drawable settingsIcon = res.getDrawable(R.drawable.ic_settings); 116 String settingsDescription = res.getString(R.string.notification_menu_gear_description); 117 NotificationInfo settingsContent = (NotificationInfo) LayoutInflater.from(context).inflate( 118 R.layout.notification_info, null, false); 119 MenuItem settings = new MenuItem(settingsIcon, settingsDescription, settingsContent); 120 return settings; 121 } 122 123 public ArrayList<MenuItem> getDefaultNotificationMenuItems() { 124 ArrayList<MenuItem> items = new ArrayList<MenuItem>(); 125 Resources res = getResources(); 126 127 Drawable snoozeIcon = res.getDrawable(R.drawable.ic_snooze); 128 NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(mContext) 129 .inflate(R.layout.notification_snooze, null, false); 130 String snoozeDescription = res.getString(R.string.notification_menu_snooze_description); 131 MenuItem snooze = new MenuItem(snoozeIcon, snoozeDescription, content); 132 items.add(snooze); 133 134 Drawable settingsIcon = res.getDrawable(R.drawable.ic_settings); 135 String settingsDescription = res.getString(R.string.notification_menu_gear_description); 136 NotificationInfo settingsContent = (NotificationInfo) LayoutInflater.from(mContext).inflate( 137 R.layout.notification_info, null, false); 138 MenuItem settings = new MenuItem(settingsIcon, settingsDescription, settingsContent); 139 items.add(settings); 140 return items; 141 } 142 143 private void updateMenu(boolean notify) { 144 removeAllViews(); 145 mMenuItems.clear(); 146 if (mMenuProvider != null) { 147 mMenuItems.addAll(mMenuProvider.getMenuItems(getContext())); 148 } 149 mMenuItems.addAll(getDefaultNotificationMenuItems()); 150 for (int i = 0; i < mMenuItems.size(); i++) { 151 final View v = createMenuView(mMenuItems.get(i)); 152 mMenuItems.get(i).menuView = v; 153 } 154 resetState(notify); 155 } 156 157 private View createMenuView(MenuItem item) { 158 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(getContext()); 159 addView(iv); 160 iv.setPadding(mIconPadding, mIconPadding, mIconPadding, mIconPadding); 161 iv.setImageDrawable(item.icon); 162 iv.setOnClickListener(this); 163 iv.setColorFilter(mIconTint); 164 iv.setAlpha(mAlpha); 165 FrameLayout.LayoutParams lp = (LayoutParams) iv.getLayoutParams(); 166 lp.width = (int) mHorizSpaceForIcon; 167 lp.height = (int) mHorizSpaceForIcon; 168 return iv; 169 } 170 171 public void resetState(boolean notify) { 172 setMenuAlpha(0f); 173 mIconsPlaced = false; 174 mMenuFadedIn = false; 175 mAnimating = false; 176 mSnapping = false; 177 mDismissing = false; 178 setMenuLocation(mOnLeft ? 1 : -1 /* on left */); 179 if (mListener != null && notify) { 180 mListener.onMenuReset(mParent); 181 } 182 } 183 184 public void setMenuClickListener(OnMenuClickListener listener) { 185 mListener = listener; 186 } 187 188 public void setNotificationRowParent(ExpandableNotificationRow parent) { 189 mParent = parent; 190 setMenuLocation(mOnLeft ? 1 : -1); 191 } 192 193 public void setAppName(String appName) { 194 Resources res = getResources(); 195 final int count = mMenuItems.size(); 196 for (int i = 0; i < count; i++) { 197 MenuItem item = mMenuItems.get(i); 198 String description = String.format( 199 res.getString(R.string.notification_menu_accessibility), 200 appName, item.menuDescription); 201 item.menuView.setContentDescription(description); 202 } 203 } 204 205 public ExpandableNotificationRow getNotificationParent() { 206 return mParent; 207 } 208 209 public void setMenuAlpha(float alpha) { 210 mAlpha = alpha; 211 if (alpha == 0) { 212 mMenuFadedIn = false; // Can fade in again once it's gone. 213 setVisibility(View.INVISIBLE); 214 } else { 215 setVisibility(View.VISIBLE); 216 } 217 final int count = getChildCount(); 218 for (int i = 0; i < count; i++) { 219 getChildAt(i).setAlpha(mAlpha); 220 } 221 } 222 223 /** 224 * Returns whether the menu is displayed on the left side of the view or not. 225 */ 226 public boolean isMenuOnLeft() { 227 return mOnLeft; 228 } 229 230 /** 231 * Returns the horizontal space in pixels required to display the menu. 232 */ 233 public float getSpaceForMenu() { 234 return mHorizSpaceForIcon * getChildCount(); 235 } 236 237 /** 238 * Indicates whether the menu is visible at 1 alpha. Does not indicate if entire view is 239 * visible. 240 */ 241 public boolean isVisible() { 242 return mAlpha > 0; 243 } 244 245 public void cancelFadeAnimator() { 246 if (mFadeAnimator != null) { 247 mFadeAnimator.cancel(); 248 } 249 } 250 251 public void updateMenuAlpha(final float transX, final float size) { 252 if (mAnimating || !mMenuFadedIn) { 253 // Don't adjust when animating, or if the menu hasn't been shown yet. 254 return; 255 } 256 257 final float fadeThreshold = size * 0.3f; 258 final float absTrans = Math.abs(transX); 259 float desiredAlpha = 0; 260 261 if (absTrans == 0) { 262 desiredAlpha = 0; 263 } else if (absTrans <= fadeThreshold) { 264 desiredAlpha = 1; 265 } else { 266 desiredAlpha = 1 - ((absTrans - fadeThreshold) / (size - fadeThreshold)); 267 } 268 setMenuAlpha(desiredAlpha); 269 } 270 271 public void fadeInMenu(final boolean fromLeft, final float transX, 272 final float notiThreshold) { 273 if (mDismissing || mAnimating) { 274 return; 275 } 276 if (isMenuLocationChange(transX)) { 277 setMenuAlpha(0f); 278 } 279 setMenuLocation((int) transX); 280 mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1); 281 mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 282 @Override 283 public void onAnimationUpdate(ValueAnimator animation) { 284 final float absTrans = Math.abs(transX); 285 286 boolean pastGear = (fromLeft && transX <= notiThreshold) 287 || (!fromLeft && absTrans <= notiThreshold); 288 if (pastGear && !mMenuFadedIn) { 289 setMenuAlpha((float) animation.getAnimatedValue()); 290 } 291 } 292 }); 293 mFadeAnimator.addListener(new AnimatorListenerAdapter() { 294 @Override 295 public void onAnimationStart(Animator animation) { 296 mAnimating = true; 297 } 298 299 @Override 300 public void onAnimationCancel(Animator animation) { 301 // TODO should animate back to 0f from current alpha 302 setMenuAlpha(0f); 303 } 304 305 @Override 306 public void onAnimationEnd(Animator animation) { 307 mAnimating = false; 308 mMenuFadedIn = mAlpha == 1; 309 } 310 }); 311 mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN); 312 mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION); 313 mFadeAnimator.start(); 314 } 315 316 public void updateVerticalLocation() { 317 if (mParent == null || mMenuItems.size() == 0) { 318 return; 319 } 320 int parentHeight = mParent.getCollapsedHeight(); 321 float translationY; 322 if (parentHeight < mVertSpaceForIcons) { 323 translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2); 324 } else { 325 translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2; 326 } 327 setTranslationY(translationY); 328 } 329 330 @Override 331 public void onRtlPropertiesChanged(int layoutDirection) { 332 mIconsPlaced = false; 333 setMenuLocation(mOnLeft ? 1 : -1); 334 } 335 336 public void setMenuLocation(int translation) { 337 boolean onLeft = translation > 0; 338 if ((mIconsPlaced && onLeft == mOnLeft) || mSnapping || mParent == null) { 339 // Do nothing 340 return; 341 } 342 final boolean isRtl = mParent.isLayoutRtl(); 343 final int count = getChildCount(); 344 final int width = getWidth(); 345 for (int i = 0; i < count; i++) { 346 final View v = getChildAt(i); 347 final float left = isRtl 348 ? -(width - mHorizSpaceForIcon * (i + 1)) 349 : i * mHorizSpaceForIcon; 350 final float right = isRtl 351 ? -i * mHorizSpaceForIcon 352 : width - (mHorizSpaceForIcon * (i + 1)); 353 v.setTranslationX(onLeft ? left : right); 354 } 355 mOnLeft = onLeft; 356 mIconsPlaced = true; 357 } 358 359 public boolean isMenuLocationChange(float translation) { 360 boolean onLeft = translation > mIconPadding; 361 boolean onRight = translation < -mIconPadding; 362 if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) { 363 return true; 364 } 365 return false; 366 } 367 368 public void setDismissing() { 369 mDismissing = true; 370 } 371 372 public void setSnapping(boolean snapping) { 373 mSnapping = snapping; 374 } 375 376 @Override 377 public void onClick(View v) { 378 if (mListener == null) { 379 // Nothing to do 380 return; 381 } 382 v.getLocationOnScreen(mIconLocation); 383 mParent.getLocationOnScreen(mParentLocation); 384 final int centerX = (int) (mHorizSpaceForIcon / 2); 385 final int centerY = (int) (v.getTranslationY() * 2 + v.getHeight()) / 2; 386 final int x = mIconLocation[0] - mParentLocation[0] + centerX; 387 final int y = mIconLocation[1] - mParentLocation[1] + centerY; 388 final int index = indexOfChild(v); 389 mListener.onMenuClicked(mParent, x, y, mMenuItems.get(index)); 390 } 391 392 @Override 393 public void onPluginConnected(NotificationMenuRowProvider plugin, Context pluginContext) { 394 mMenuProvider = plugin; 395 updateMenu(false /* notify */); 396 } 397 398 @Override 399 public void onPluginDisconnected(NotificationMenuRowProvider plugin) { 400 mMenuProvider = null; 401 updateMenu(false /* notify */); 402 } 403}