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 */ 16 package com.android.contacts.widget; 17 18 import android.database.CharArrayBuffer; 19 import android.graphics.Color; 20 import android.os.Handler; 21 import android.text.TextPaint; 22 import android.text.style.CharacterStyle; 23 import android.view.animation.AccelerateInterpolator; 24 import android.view.animation.DecelerateInterpolator; 25 26 import com.android.contacts.common.format.FormatUtils; 27 import com.android.internal.R; 28 29 /** 30 * An animation that alternately dims and brightens the non-highlighted portion of text. 31 */ 32 public 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