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.util.AttributeSet;
25import android.view.View;
26import android.widget.FrameLayout;
27
28import com.android.systemui.Interpolators;
29import com.android.systemui.R;
30
31public 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