1/*
2 * Copyright (C) 2010 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 */
16package com.android.contacts.widget;
17
18import android.database.CharArrayBuffer;
19import android.graphics.Color;
20import android.os.Handler;
21import android.text.TextPaint;
22import android.text.style.CharacterStyle;
23import android.view.animation.AccelerateInterpolator;
24import android.view.animation.DecelerateInterpolator;
25
26import com.android.contacts.format.FormatUtils;
27import com.android.internal.R;
28
29/**
30 * An animation that alternately dims and brightens the non-highlighted portion of text.
31 */
32public abstract class TextHighlightingAnimation implements Runnable, TextWithHighlightingFactory {
33
34    private static final int MAX_ALPHA = 255;
35    private static final int MIN_ALPHA = 50;
36
37    private AccelerateInterpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
38    private DecelerateInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
39
40    private final static DimmingSpan[] sEmptySpans = new DimmingSpan[0];
41
42    /**
43     * Frame rate expressed a number of millis between frames.
44     */
45    private static final long FRAME_RATE = 50;
46
47    private DimmingSpan mDimmingSpan;
48    private Handler mHandler;
49    private boolean mAnimating;
50    private boolean mDimming;
51    private long mTargetTime;
52    private final int mDuration;
53
54    /**
55     * A Spanned that highlights a part of text by dimming another part of that text.
56     */
57    public class TextWithHighlightingImpl implements TextWithHighlighting {
58
59        private final DimmingSpan[] mSpans;
60        private boolean mDimmingEnabled;
61        private CharArrayBuffer mText;
62        private int mDimmingSpanStart;
63        private int mDimmingSpanEnd;
64        private String mString;
65
66        public TextWithHighlightingImpl() {
67            mSpans = new DimmingSpan[] { mDimmingSpan };
68        }
69
70        public void setText(CharArrayBuffer baseText, CharArrayBuffer highlightedText) {
71            mText = baseText;
72
73            // TODO figure out a way to avoid string allocation
74            mString = new String(mText.data, 0, mText.sizeCopied);
75
76            int index = FormatUtils.overlapPoint(baseText, highlightedText);
77
78            if (index == 0 || index == -1) {
79                mDimmingEnabled = false;
80            } else {
81                mDimmingEnabled = true;
82                mDimmingSpanStart = 0;
83                mDimmingSpanEnd = index;
84            }
85        }
86
87        @SuppressWarnings("unchecked")
88        public <T> T[] getSpans(int start, int end, Class<T> type) {
89            if (mDimmingEnabled) {
90                return (T[])mSpans;
91            } else {
92                return (T[])sEmptySpans;
93            }
94        }
95
96        public int getSpanStart(Object tag) {
97            // We only have one span - no need to check the tag parameter
98            return mDimmingSpanStart;
99        }
100
101        public int getSpanEnd(Object tag) {
102            // We only have one span - no need to check the tag parameter
103            return mDimmingSpanEnd;
104        }
105
106        public int getSpanFlags(Object tag) {
107            // String is immutable - flags not needed
108            return 0;
109        }
110
111        public int nextSpanTransition(int start, int limit, Class type) {
112            // Never called since we only have one span
113            return 0;
114        }
115
116        public char charAt(int index) {
117            return mText.data[index];
118        }
119
120        public int length() {
121            return mText.sizeCopied;
122        }
123
124        public CharSequence subSequence(int start, int end) {
125            // Never called - implementing for completeness
126            return new String(mText.data, start, end);
127        }
128
129        @Override
130        public String toString() {
131            return mString;
132        }
133    }
134
135    /**
136     * A Span that modifies alpha of the default foreground color.
137     */
138    private static class DimmingSpan extends CharacterStyle {
139        private int mAlpha;
140
141        public void setAlpha(int alpha) {
142            mAlpha = alpha;
143        }
144
145        @Override
146        public void updateDrawState(TextPaint ds) {
147
148            // Only dim the text in the basic state; not selected, focused or pressed
149            int[] states = ds.drawableState;
150            if (states != null) {
151                int count = states.length;
152                for (int i = 0; i < count; i++) {
153                    switch (states[i]) {
154                        case R.attr.state_pressed:
155                        case R.attr.state_selected:
156                        case R.attr.state_focused:
157                            // We can simply return, because the supplied text
158                            // paint is already configured with defaults.
159                            return;
160                    }
161                }
162            }
163
164            int color = ds.getColor();
165            color = Color.argb(mAlpha, Color.red(color), Color.green(color), Color.blue(color));
166            ds.setColor(color);
167        }
168    }
169
170    /**
171     * Constructor.
172     */
173    public TextHighlightingAnimation(int duration) {
174        mDuration = duration;
175        mHandler = new Handler();
176        mDimmingSpan = new DimmingSpan();
177        mDimmingSpan.setAlpha(MAX_ALPHA);
178    }
179
180    /**
181     * Returns a Spanned that can be used by a text view to show text with highlighting.
182     */
183    public TextWithHighlightingImpl createTextWithHighlighting() {
184        return new TextWithHighlightingImpl();
185    }
186
187    /**
188     * Override and invalidate (redraw) TextViews showing {@link TextWithHighlightingImpl}.
189     */
190    protected abstract void invalidate();
191
192    /**
193     * Starts the highlighting animation, which will dim portions of text.
194     */
195    public void startHighlighting() {
196        startAnimation(true);
197    }
198
199    /**
200     * Starts un-highlighting animation, which will brighten the dimmed portions of text
201     * to the brightness level of the rest of text.
202     */
203    public void stopHighlighting() {
204        startAnimation(false);
205    }
206
207    /**
208     * Called when the animation starts.
209     */
210    protected void onAnimationStarted() {
211    }
212
213    /**
214     * Called when the animation has stopped.
215     */
216    protected void onAnimationEnded() {
217    }
218
219    private void startAnimation(boolean dim) {
220        if (mDimming != dim) {
221            mDimming = dim;
222            long now = System.currentTimeMillis();
223            if (!mAnimating) {
224                mAnimating = true;
225                mTargetTime = now + mDuration;
226                onAnimationStarted();
227                mHandler.post(this);
228            } else  {
229
230                // If we have started dimming, reverse the direction and adjust the target
231                // time accordingly.
232                mTargetTime = (now + mDuration) - (mTargetTime - now);
233            }
234        }
235    }
236
237    /**
238     * Animation step.
239     */
240    public void run() {
241        long now = System.currentTimeMillis();
242        long timeLeft = mTargetTime - now;
243        if (timeLeft < 0) {
244            mDimmingSpan.setAlpha(mDimming ? MIN_ALPHA : MAX_ALPHA);
245            mAnimating = false;
246            onAnimationEnded();
247            return;
248        }
249
250        // Start=1, end=0
251        float virtualTime = (float)timeLeft / mDuration;
252        if (mDimming) {
253            float interpolatedTime = DECELERATE_INTERPOLATOR.getInterpolation(virtualTime);
254            mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * interpolatedTime));
255        } else {
256            float interpolatedTime = ACCELERATE_INTERPOLATOR.getInterpolation(virtualTime);
257            mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * (1-interpolatedTime)));
258        }
259
260        invalidate();
261
262        // Repeat
263        mHandler.postDelayed(this, FRAME_RATE);
264    }
265}
266