blob: e35ae1e4bc686c8286675b5720809d4c58deb4ab [file] [log] [blame]
Dmitri Plotnikovdaa2d5c2010-01-29 17:44:20 -08001/*
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;
17
18import com.android.internal.R;
19
20import android.database.CharArrayBuffer;
21import android.graphics.Color;
22import android.os.Handler;
23import android.text.Spanned;
24import android.text.TextPaint;
25import android.text.style.CharacterStyle;
26import android.view.animation.AccelerateInterpolator;
27import android.view.animation.DecelerateInterpolator;
28
29/**
30 * An animation that alternately dims and brightens the non-highlighted portion of text.
31 */
32public abstract class TextHighlightingAnimation implements Runnable {
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
Dmitri Plotnikov0aa25b92010-03-16 16:55:17 -070042 /**
43 * Frame rate expressed a number of millis between frames.
44 */
45 private static final long FRAME_RATE = 50;
46
Dmitri Plotnikovdaa2d5c2010-01-29 17:44:20 -080047 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 TextWithHighlighting implements Spanned {
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 TextWithHighlighting() {
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 = indexOf(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 /**
88 * An implementation of indexOf on CharArrayBuffers that finds the first match of
89 * the start of buffer2 in buffer1. For example, indexOf("abcd", "cdef") == 2
90 */
91 private int indexOf(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
92 char[] string1 = buffer1.data;
93 char[] string2 = buffer2.data;
94 int count1 = buffer1.sizeCopied;
95 int count2 = buffer2.sizeCopied;
Dmitri Plotnikov0aa25b92010-03-16 16:55:17 -070096
97 // Ignore matching tails of the two buffers
98 while (count1 > 0 && count2 > 0 && string1[count1 - 1] == string2[count2 - 1]) {
99 count1--;
100 count2--;
101 }
102
Dmitri Plotnikovdaa2d5c2010-01-29 17:44:20 -0800103 int size = count2;
104 for (int i = 0; i < count1; i++) {
105 if (i + size > count1) {
106 size = count1 - i;
107 }
108 int j;
109 for (j = 0; j < size; j++) {
110 if (string1[i+j] != string2[j]) {
111 break;
112 }
113 }
114 if (j == size) {
115 return i;
116 }
117 }
118
119 return -1;
120 }
121
122
123 @SuppressWarnings("unchecked")
124 public <T> T[] getSpans(int start, int end, Class<T> type) {
125 if (mDimmingEnabled) {
126 return (T[])mSpans;
127 } else {
128 return (T[])sEmptySpans;
129 }
130 }
131
132 public int getSpanStart(Object tag) {
133 // We only have one span - no need to check the tag parameter
134 return mDimmingSpanStart;
135 }
136
137 public int getSpanEnd(Object tag) {
138 // We only have one span - no need to check the tag parameter
139 return mDimmingSpanEnd;
140 }
141
142 public int getSpanFlags(Object tag) {
143 // String is immutable - flags not needed
144 return 0;
145 }
146
147 public int nextSpanTransition(int start, int limit, Class type) {
148 // Never called since we only have one span
149 return 0;
150 }
151
152 public char charAt(int index) {
153 return mText.data[index];
154 }
155
156 public int length() {
157 return mText.sizeCopied;
158 }
159
160 public CharSequence subSequence(int start, int end) {
161 // Never called - implementing for completeness
162 return new String(mText.data, start, end);
163 }
164
165 @Override
166 public String toString() {
167 return mString;
168 }
169 }
170
171 /**
172 * A Span that modifies alpha of the default foreground color.
173 */
174 private static class DimmingSpan extends CharacterStyle {
175 private int mAlpha;
176
177 public void setAlpha(int alpha) {
178 mAlpha = alpha;
179 }
180
181 @Override
182 public void updateDrawState(TextPaint ds) {
183
184 // Only dim the text in the basic state; not selected, focused or pressed
185 int[] states = ds.drawableState;
186 if (states != null) {
187 int count = states.length;
188 for (int i = 0; i < count; i++) {
189 switch (states[i]) {
190 case R.attr.state_pressed:
191 case R.attr.state_selected:
192 case R.attr.state_focused:
193 // We can simply return, because the supplied text
194 // paint is already configured with defaults.
195 return;
196 }
197 }
198 }
199
200 int color = ds.getColor();
201 color = Color.argb(mAlpha, Color.red(color), Color.green(color), Color.blue(color));
202 ds.setColor(color);
203 }
204 }
205
206 /**
207 * Constructor.
208 */
209 public TextHighlightingAnimation(int duration) {
210 mDuration = duration;
211 mHandler = new Handler();
212 mDimmingSpan = new DimmingSpan();
213 mDimmingSpan.setAlpha(MAX_ALPHA);
214 }
215
216 /**
217 * Returns a Spanned that can be used by a text view to show text with highlighting.
218 */
219 public TextWithHighlighting createTextWithHighlighting() {
220 return new TextWithHighlighting();
221 }
222
223 /**
224 * Override and invalidate (redraw) TextViews showing {@link TextWithHighlighting}.
225 */
226 protected abstract void invalidate();
227
228 /**
229 * Starts the highlighting animation, which will dim portions of text.
230 */
231 public void startHighlighting() {
232 startAnimation(true);
233 }
234
235 /**
236 * Starts un-highlighting animation, which will brighten the dimmed portions of text
237 * to the brightness level of the rest of text.
238 */
239 public void stopHighlighting() {
240 startAnimation(false);
241 }
242
243 /**
244 * Called when the animation starts.
245 */
246 protected void onAnimationStarted() {
247 }
248
249 /**
250 * Called when the animation has stopped.
251 */
252 protected void onAnimationEnded() {
253 }
254
255 private void startAnimation(boolean dim) {
256 if (mDimming != dim) {
257 mDimming = dim;
258 long now = System.currentTimeMillis();
259 if (!mAnimating) {
260 mAnimating = true;
261 mTargetTime = now + mDuration;
262 onAnimationStarted();
263 mHandler.post(this);
264 } else {
265
266 // If we have started dimming, reverse the direction and adjust the target
267 // time accordingly.
268 mTargetTime = (now + mDuration) - (mTargetTime - now);
269 }
270 }
271 }
272
273 /**
274 * Animation step.
275 */
276 public void run() {
277 long now = System.currentTimeMillis();
278 long timeLeft = mTargetTime - now;
279 if (timeLeft < 0) {
280 mDimmingSpan.setAlpha(mDimming ? MIN_ALPHA : MAX_ALPHA);
281 mAnimating = false;
282 onAnimationEnded();
283 return;
284 }
285
286 // Start=1, end=0
287 float virtualTime = (float)timeLeft / mDuration;
288 if (mDimming) {
289 float interpolatedTime = DECELERATE_INTERPOLATOR.getInterpolation(virtualTime);
290 mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * interpolatedTime));
291 } else {
292 float interpolatedTime = ACCELERATE_INTERPOLATOR.getInterpolation(virtualTime);
293 mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * (1-interpolatedTime)));
294 }
295
296 invalidate();
297
298 // Repeat
Dmitri Plotnikov0aa25b92010-03-16 16:55:17 -0700299 mHandler.postDelayed(this, FRAME_RATE);
Dmitri Plotnikovdaa2d5c2010-01-29 17:44:20 -0800300 }
Dmitri Plotnikov0aa25b92010-03-16 16:55:17 -0700301}