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 android.support.v7.app;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Rect;
22import android.graphics.drawable.BitmapDrawable;
23import android.util.AttributeSet;
24import android.view.animation.Interpolator;
25import android.widget.ListView;
26
27import java.util.ArrayList;
28import java.util.Iterator;
29import java.util.List;
30
31/**
32 * A ListView which has an additional overlay layer. {@link BitmapDrawable}
33 * can be added to the layer and can be animated.
34 */
35final class OverlayListView extends ListView {
36    private final List<OverlayObject> mOverlayObjects = new ArrayList<>();
37
38    public OverlayListView(Context context) {
39        super(context);
40    }
41
42    public OverlayListView(Context context, AttributeSet attrs) {
43        super(context, attrs);
44    }
45
46    public OverlayListView(Context context, AttributeSet attrs, int defStyleAttr) {
47        super(context, attrs, defStyleAttr);
48    }
49
50    /**
51     * Adds an object to the overlay layer.
52     *
53     * @param object An object to be added.
54     */
55    public void addOverlayObject(OverlayObject object) {
56        mOverlayObjects.add(object);
57    }
58
59    /**
60     * Starts all animations of objects in the overlay layer.
61     */
62    public void startAnimationAll() {
63        for (OverlayObject object : mOverlayObjects) {
64            if (!object.isAnimationStarted()) {
65                object.startAnimation(getDrawingTime());
66            }
67        }
68    }
69
70    /**
71     * Stops all animations of objects in the overlay layer.
72     */
73    public void stopAnimationAll() {
74        for (OverlayObject object : mOverlayObjects) {
75            object.stopAnimation();
76        }
77    }
78
79    @Override
80    public void onDraw(Canvas canvas) {
81        super.onDraw(canvas);
82        if (mOverlayObjects.size() > 0) {
83            Iterator<OverlayObject> it = mOverlayObjects.iterator();
84            while (it.hasNext()) {
85                OverlayObject object = it.next();
86                BitmapDrawable bitmap = object.getBitmapDrawable();
87                if (bitmap != null) {
88                    bitmap.draw(canvas);
89                }
90                if (!object.update(getDrawingTime())) {
91                    it.remove();
92                }
93            }
94        }
95    }
96
97    /**
98     * A class that represents an object to be shown in the overlay layer.
99     */
100    public static class OverlayObject {
101        private BitmapDrawable mBitmap;
102        private float mCurrentAlpha = 1.0f;
103        private Rect mCurrentBounds;
104        private Interpolator mInterpolator;
105        private long mDuration;
106        private Rect mStartRect;
107        private int mDeltaY;
108        private float mStartAlpha = 1.0f;
109        private float mEndAlpha = 1.0f;
110        private long mStartTime;
111        private boolean mIsAnimationStarted;
112        private boolean mIsAnimationEnded;
113        private OnAnimationEndListener mListener;
114
115        public OverlayObject(BitmapDrawable bitmap, Rect startRect) {
116            mBitmap = bitmap;
117            mStartRect = startRect;
118            mCurrentBounds = new Rect(startRect);
119            if (mBitmap != null && mCurrentBounds != null) {
120                mBitmap.setAlpha((int) (mCurrentAlpha * 255));
121                mBitmap.setBounds(mCurrentBounds);
122            }
123        }
124
125        /**
126         * Returns the bitmap that this object represents.
127         *
128         * @return BitmapDrawable that this object has.
129         */
130        public BitmapDrawable getBitmapDrawable() {
131            return mBitmap;
132        }
133
134        /**
135         * Returns the started status of the animation.
136         *
137         * @return True if the animation has started, false otherwise.
138         */
139        public boolean isAnimationStarted() {
140            return mIsAnimationStarted;
141        }
142
143        /**
144         * Sets animation for varying alpha.
145         *
146         * @param startAlpha Starting alpha value for the animation, where 1.0 means
147         * fully opaque and 0.0 means fully transparent.
148         * @param endAlpha Ending alpha value for the animation.
149         * @return This OverlayObject to allow for chaining of calls.
150         */
151        public OverlayObject setAlphaAnimation(float startAlpha, float endAlpha) {
152            mStartAlpha = startAlpha;
153            mEndAlpha = endAlpha;
154            return this;
155        }
156
157        /**
158         * Sets animation for moving objects vertically.
159         *
160         * @param deltaY Distance to move in pixels.
161         * @return This OverlayObject to allow for chaining of calls.
162         */
163        public OverlayObject setTranslateYAnimation(int deltaY) {
164            mDeltaY = deltaY;
165            return this;
166        }
167
168        /**
169         * Sets how long the animation will last.
170         *
171         * @param duration Duration in milliseconds
172         * @return This OverlayObject to allow for chaining of calls.
173         */
174        public OverlayObject setDuration(long duration) {
175            mDuration = duration;
176            return this;
177        }
178
179        /**
180         * Sets the acceleration curve for this animation.
181         *
182         * @param interpolator The interpolator which defines the acceleration curve
183         * @return This OverlayObject to allow for chaining of calls.
184         */
185        public OverlayObject setInterpolator(Interpolator interpolator) {
186            mInterpolator = interpolator;
187            return this;
188        }
189
190        /**
191         * Binds an animation end listener to the animation.
192         *
193         * @param listener the animation end listener to be notified.
194         * @return This OverlayObject to allow for chaining of calls.
195         */
196        public OverlayObject setAnimationEndListener(OnAnimationEndListener listener) {
197            mListener = listener;
198            return this;
199        }
200
201        /**
202         * Starts the animation and sets the start time.
203         *
204         * @param startTime Start time to be set in Millis
205         */
206        public void startAnimation(long startTime) {
207            mStartTime = startTime;
208            mIsAnimationStarted = true;
209        }
210
211        /**
212         * Stops the animation.
213         */
214        public void stopAnimation() {
215            mIsAnimationStarted = true;
216            mIsAnimationEnded = true;
217            if (mListener != null) {
218                mListener.onAnimationEnd();
219            }
220        }
221
222        /**
223         * Calculates and updates current bounds and alpha value.
224         *
225         * @param currentTime Current time.in millis
226         */
227        public boolean update(long currentTime) {
228            if (mIsAnimationEnded) {
229                return false;
230            }
231            float normalizedTime = (currentTime - mStartTime) / (float) mDuration;
232            normalizedTime = Math.max(0.0f, Math.min(1.0f, normalizedTime));
233            if (!mIsAnimationStarted) {
234                normalizedTime = 0.0f;
235            }
236            float interpolatedTime = (mInterpolator == null) ? normalizedTime
237                    : mInterpolator.getInterpolation(normalizedTime);
238            int deltaY = (int) (mDeltaY * interpolatedTime);
239            mCurrentBounds.top = mStartRect.top + deltaY;
240            mCurrentBounds.bottom = mStartRect.bottom + deltaY;
241            mCurrentAlpha = mStartAlpha + (mEndAlpha - mStartAlpha) * interpolatedTime;
242            if (mBitmap != null && mCurrentBounds != null) {
243                mBitmap.setAlpha((int) (mCurrentAlpha * 255));
244                mBitmap.setBounds(mCurrentBounds);
245            }
246            if (mIsAnimationStarted && normalizedTime >= 1.0f) {
247                mIsAnimationEnded = true;
248                if (mListener != null) {
249                    mListener.onAnimationEnd();
250                }
251            }
252            return !mIsAnimationEnded;
253        }
254
255        /**
256         * An animation listener that receives notifications when the animation ends.
257         */
258        public interface OnAnimationEndListener {
259            /**
260             * Notifies the end of the animation.
261             */
262            public void onAnimationEnd();
263        }
264    }
265}
266