/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Resources; import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; import com.android.systemui.Interpolators; import com.android.systemui.R; public class NotificationSettingsIconRow extends FrameLayout implements View.OnClickListener { private static final int GEAR_ALPHA_ANIM_DURATION = 200; public interface SettingsIconRowListener { /** * Called when the gear behind a notification is touched. */ public void onGearTouched(ExpandableNotificationRow row, int x, int y); /** * Called when a notification is slid back over the gear. */ public void onSettingsIconRowReset(ExpandableNotificationRow row); } private ExpandableNotificationRow mParent; private AlphaOptimizedImageView mGearIcon; private float mHorizSpaceForGear; private SettingsIconRowListener mListener; private ValueAnimator mFadeAnimator; private boolean mSettingsFadedIn = false; private boolean mAnimating = false; private boolean mOnLeft = true; private boolean mDismissing = false; private boolean mSnapping = false; private boolean mIconPlaced = false; private int[] mGearLocation = new int[2]; private int[] mParentLocation = new int[2]; private int mVertSpaceForGear; public NotificationSettingsIconRow(Context context) { this(context, null); } public NotificationSettingsIconRow(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NotificationSettingsIconRow(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public NotificationSettingsIconRow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs); } @Override protected void onFinishInflate() { super.onFinishInflate(); mGearIcon = (AlphaOptimizedImageView) findViewById(R.id.gear_icon); mGearIcon.setOnClickListener(this); setOnClickListener(this); mHorizSpaceForGear = getResources().getDimensionPixelOffset(R.dimen.notification_gear_width); mVertSpaceForGear = getResources().getDimensionPixelOffset(R.dimen.notification_min_height); resetState(); } public void resetState() { setGearAlpha(0f); mIconPlaced = false; mSettingsFadedIn = false; mAnimating = false; mSnapping = false; mDismissing = false; setIconLocation(true /* on left */); if (mListener != null) { mListener.onSettingsIconRowReset(mParent); } } public void setGearListener(SettingsIconRowListener listener) { mListener = listener; } public void setNotificationRowParent(ExpandableNotificationRow parent) { mParent = parent; setIconLocation(mOnLeft); } public void setAppName(String appName) { Resources res = getResources(); String description = String.format(res.getString(R.string.notification_gear_accessibility), appName); mGearIcon.setContentDescription(description); } public ExpandableNotificationRow getNotificationParent() { return mParent; } public void setGearAlpha(float alpha) { if (alpha == 0) { mSettingsFadedIn = false; // Can fade in again once it's gone. setVisibility(View.INVISIBLE); } else { setVisibility(View.VISIBLE); } mGearIcon.setAlpha(alpha); } /** * Returns whether the icon is on the left side of the view or not. */ public boolean isIconOnLeft() { return mOnLeft; } /** * Returns the horizontal space in pixels required to display the gear behind a notification. */ public float getSpaceForGear() { return mHorizSpaceForGear; } /** * Indicates whether the gear is visible at 1 alpha. Does not indicate * if entire view is visible. */ public boolean isVisible() { return mGearIcon.getAlpha() > 0; } public void cancelFadeAnimator() { if (mFadeAnimator != null) { mFadeAnimator.cancel(); } } public void updateSettingsIcons(final float transX, final float size) { if (mAnimating || !mSettingsFadedIn) { // Don't adjust when animating, or if the gear hasn't been shown yet. return; } final float fadeThreshold = size * 0.3f; final float absTrans = Math.abs(transX); float desiredAlpha = 0; if (absTrans == 0) { desiredAlpha = 0; } else if (absTrans <= fadeThreshold) { desiredAlpha = 1; } else { desiredAlpha = 1 - ((absTrans - fadeThreshold) / (size - fadeThreshold)); } setGearAlpha(desiredAlpha); } public void fadeInSettings(final boolean fromLeft, final float transX, final float notiThreshold) { if (mDismissing || mAnimating) { return; } if (isIconLocationChange(transX)) { setGearAlpha(0f); } setIconLocation(transX > 0 /* fromLeft */); mFadeAnimator = ValueAnimator.ofFloat(mGearIcon.getAlpha(), 1); mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { final float absTrans = Math.abs(transX); boolean pastGear = (fromLeft && transX <= notiThreshold) || (!fromLeft && absTrans <= notiThreshold); if (pastGear && !mSettingsFadedIn) { setGearAlpha((float) animation.getAnimatedValue()); } } }); mFadeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mAnimating = true; } @Override public void onAnimationCancel(Animator animation) { // TODO should animate back to 0f from current alpha mGearIcon.setAlpha(0f); } @Override public void onAnimationEnd(Animator animation) { mAnimating = false; mSettingsFadedIn = mGearIcon.getAlpha() == 1; } }); mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN); mFadeAnimator.setDuration(GEAR_ALPHA_ANIM_DURATION); mFadeAnimator.start(); } public void updateVerticalLocation() { if (mParent == null) { return; } int parentHeight = mParent.getCollapsedHeight(); if (parentHeight < mVertSpaceForGear) { mGearIcon.setTranslationY((parentHeight / 2) - (mGearIcon.getHeight() / 2)); } else { mGearIcon.setTranslationY((mVertSpaceForGear - mGearIcon.getHeight()) / 2); } } @Override public void onRtlPropertiesChanged(int layoutDirection) { setIconLocation(mOnLeft); } public void setIconLocation(boolean onLeft) { if ((mIconPlaced && onLeft == mOnLeft) || mSnapping || mParent == null || mGearIcon.getWidth() == 0) { // Do nothing return; } final boolean isRtl = mParent.isLayoutRtl(); // TODO No need to cast to float here once b/28050538 is fixed. final float left = (float) (isRtl ? -(mParent.getWidth() - mHorizSpaceForGear) : 0); final float right = (float) (isRtl ? 0 : (mParent.getWidth() - mHorizSpaceForGear)); final float centerX = ((mHorizSpaceForGear - mGearIcon.getWidth()) / 2); setTranslationX(onLeft ? left + centerX : right + centerX); mOnLeft = onLeft; mIconPlaced = true; } public boolean isIconLocationChange(float translation) { boolean onLeft = translation > mGearIcon.getPaddingStart(); boolean onRight = translation < -mGearIcon.getPaddingStart(); if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) { return true; } return false; } public void setDismissing() { mDismissing = true; } public void setSnapping(boolean snapping) { mSnapping = snapping; } @Override public void onClick(View v) { if (v.getId() == R.id.gear_icon) { if (mListener != null) { mGearIcon.getLocationOnScreen(mGearLocation); mParent.getLocationOnScreen(mParentLocation); final int centerX = (int) (mHorizSpaceForGear / 2); final int centerY = (int) (mGearIcon.getTranslationY() * 2 + mGearIcon.getHeight())/ 2; final int x = mGearLocation[0] - mParentLocation[0] + centerX; final int y = mGearLocation[1] - mParentLocation[1] + centerY; mListener.onGearTouched(mParent, x, y); } } else { // Do nothing when the background is touched. } } }