blob: e35ae1e4bc686c8286675b5720809d4c58deb4ab [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.contacts;
import com.android.internal.R;
import android.database.CharArrayBuffer;
import android.graphics.Color;
import android.os.Handler;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
/**
* An animation that alternately dims and brightens the non-highlighted portion of text.
*/
public abstract class TextHighlightingAnimation implements Runnable {
private static final int MAX_ALPHA = 255;
private static final int MIN_ALPHA = 50;
private AccelerateInterpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
private DecelerateInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
private final static DimmingSpan[] sEmptySpans = new DimmingSpan[0];
/**
* Frame rate expressed a number of millis between frames.
*/
private static final long FRAME_RATE = 50;
private DimmingSpan mDimmingSpan;
private Handler mHandler;
private boolean mAnimating;
private boolean mDimming;
private long mTargetTime;
private final int mDuration;
/**
* A Spanned that highlights a part of text by dimming another part of that text.
*/
public class TextWithHighlighting implements Spanned {
private final DimmingSpan[] mSpans;
private boolean mDimmingEnabled;
private CharArrayBuffer mText;
private int mDimmingSpanStart;
private int mDimmingSpanEnd;
private String mString;
public TextWithHighlighting() {
mSpans = new DimmingSpan[] { mDimmingSpan };
}
public void setText(CharArrayBuffer baseText, CharArrayBuffer highlightedText) {
mText = baseText;
// TODO figure out a way to avoid string allocation
mString = new String(mText.data, 0, mText.sizeCopied);
int index = indexOf(baseText, highlightedText);
if (index == 0 || index == -1) {
mDimmingEnabled = false;
} else {
mDimmingEnabled = true;
mDimmingSpanStart = 0;
mDimmingSpanEnd = index;
}
}
/**
* An implementation of indexOf on CharArrayBuffers that finds the first match of
* the start of buffer2 in buffer1. For example, indexOf("abcd", "cdef") == 2
*/
private int indexOf(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
char[] string1 = buffer1.data;
char[] string2 = buffer2.data;
int count1 = buffer1.sizeCopied;
int count2 = buffer2.sizeCopied;
// Ignore matching tails of the two buffers
while (count1 > 0 && count2 > 0 && string1[count1 - 1] == string2[count2 - 1]) {
count1--;
count2--;
}
int size = count2;
for (int i = 0; i < count1; i++) {
if (i + size > count1) {
size = count1 - i;
}
int j;
for (j = 0; j < size; j++) {
if (string1[i+j] != string2[j]) {
break;
}
}
if (j == size) {
return i;
}
}
return -1;
}
@SuppressWarnings("unchecked")
public <T> T[] getSpans(int start, int end, Class<T> type) {
if (mDimmingEnabled) {
return (T[])mSpans;
} else {
return (T[])sEmptySpans;
}
}
public int getSpanStart(Object tag) {
// We only have one span - no need to check the tag parameter
return mDimmingSpanStart;
}
public int getSpanEnd(Object tag) {
// We only have one span - no need to check the tag parameter
return mDimmingSpanEnd;
}
public int getSpanFlags(Object tag) {
// String is immutable - flags not needed
return 0;
}
public int nextSpanTransition(int start, int limit, Class type) {
// Never called since we only have one span
return 0;
}
public char charAt(int index) {
return mText.data[index];
}
public int length() {
return mText.sizeCopied;
}
public CharSequence subSequence(int start, int end) {
// Never called - implementing for completeness
return new String(mText.data, start, end);
}
@Override
public String toString() {
return mString;
}
}
/**
* A Span that modifies alpha of the default foreground color.
*/
private static class DimmingSpan extends CharacterStyle {
private int mAlpha;
public void setAlpha(int alpha) {
mAlpha = alpha;
}
@Override
public void updateDrawState(TextPaint ds) {
// Only dim the text in the basic state; not selected, focused or pressed
int[] states = ds.drawableState;
if (states != null) {
int count = states.length;
for (int i = 0; i < count; i++) {
switch (states[i]) {
case R.attr.state_pressed:
case R.attr.state_selected:
case R.attr.state_focused:
// We can simply return, because the supplied text
// paint is already configured with defaults.
return;
}
}
}
int color = ds.getColor();
color = Color.argb(mAlpha, Color.red(color), Color.green(color), Color.blue(color));
ds.setColor(color);
}
}
/**
* Constructor.
*/
public TextHighlightingAnimation(int duration) {
mDuration = duration;
mHandler = new Handler();
mDimmingSpan = new DimmingSpan();
mDimmingSpan.setAlpha(MAX_ALPHA);
}
/**
* Returns a Spanned that can be used by a text view to show text with highlighting.
*/
public TextWithHighlighting createTextWithHighlighting() {
return new TextWithHighlighting();
}
/**
* Override and invalidate (redraw) TextViews showing {@link TextWithHighlighting}.
*/
protected abstract void invalidate();
/**
* Starts the highlighting animation, which will dim portions of text.
*/
public void startHighlighting() {
startAnimation(true);
}
/**
* Starts un-highlighting animation, which will brighten the dimmed portions of text
* to the brightness level of the rest of text.
*/
public void stopHighlighting() {
startAnimation(false);
}
/**
* Called when the animation starts.
*/
protected void onAnimationStarted() {
}
/**
* Called when the animation has stopped.
*/
protected void onAnimationEnded() {
}
private void startAnimation(boolean dim) {
if (mDimming != dim) {
mDimming = dim;
long now = System.currentTimeMillis();
if (!mAnimating) {
mAnimating = true;
mTargetTime = now + mDuration;
onAnimationStarted();
mHandler.post(this);
} else {
// If we have started dimming, reverse the direction and adjust the target
// time accordingly.
mTargetTime = (now + mDuration) - (mTargetTime - now);
}
}
}
/**
* Animation step.
*/
public void run() {
long now = System.currentTimeMillis();
long timeLeft = mTargetTime - now;
if (timeLeft < 0) {
mDimmingSpan.setAlpha(mDimming ? MIN_ALPHA : MAX_ALPHA);
mAnimating = false;
onAnimationEnded();
return;
}
// Start=1, end=0
float virtualTime = (float)timeLeft / mDuration;
if (mDimming) {
float interpolatedTime = DECELERATE_INTERPOLATOR.getInterpolation(virtualTime);
mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * interpolatedTime));
} else {
float interpolatedTime = ACCELERATE_INTERPOLATOR.getInterpolation(virtualTime);
mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * (1-interpolatedTime)));
}
invalidate();
// Repeat
mHandler.postDelayed(this, FRAME_RATE);
}
}