package cn.zhaiyifan.lyric.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.os.Handler;
import android.text.method.ScrollingMovementMethod;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.TextView;
import java.util.LinkedList;
import java.util.List;
import cn.zhaiyifan.lyric.LyricUtils;
import cn.zhaiyifan.lyric.model.Lyric;
/**
* A Scrollable TextView which use lyric stream as input and display it.
* <p/>
* Created by yifan on 5/13/14.
*/
public class LyricView extends TextView implements Runnable {
public Lyric lyric;
private static final int DY = 50;
private Paint mCurrentPaint;
private Paint mPaint;
private float mMiddleX;
private float mMiddleY;
private int mHeight;
private int mLyricIndex = 0;
private int mLyricSentenceLength;
private boolean mIsNeedUpdate = false;
private float mLastEffectY = 0;
private int mIsTouched = 0;
private OnLyricUpdateListener mOnLyricUpdateListener;
public LyricView(Context context) {
this(context, null);
}
public LyricView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LyricView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setFocusable(true);
int backgroundColor = Color.BLACK;
int highlightColor = Color.RED;
int normalColor = Color.WHITE;
setBackgroundColor(backgroundColor);
// Non-highlight part
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setTextSize(36);
mPaint.setColor(normalColor);
mPaint.setTypeface(Typeface.SERIF);
// highlight part, current lyric
mCurrentPaint = new Paint();
mCurrentPaint.setAntiAlias(true);
mCurrentPaint.setColor(highlightColor);
mCurrentPaint.setTextSize(36);
mCurrentPaint.setTypeface(Typeface.SANS_SERIF);
mPaint.setTextAlign(Paint.Align.CENTER);
mCurrentPaint.setTextAlign(Paint.Align.CENTER);
setHorizontallyScrolling(true);
setMovementMethod(new ScrollingMovementMethod());
}
public void setOnLyricUpdateListener(OnLyricUpdateListener lister) {
mOnLyricUpdateListener = lister;
}
private int drawText(Canvas canvas, Paint paint, String text, float startY) {
int line = 0;
float textWidth = mPaint.measureText(text);
final int width = getWidth() - 85;
if (textWidth > width) {
int length = text.length();
int startIndex = 0;
int endIndex = Math.min((int) ((float) length * (width / textWidth)), length - 1);
int perLineLength = endIndex - startIndex;
LinkedList<String> lines = new LinkedList<>();
lines.add(text.substring(startIndex, endIndex));
while (endIndex < length - 1) {
startIndex = endIndex;
endIndex = Math.min(startIndex + perLineLength, length - 1);
lines.add(text.substring(startIndex, endIndex));
}
int linesLength = lines.size();
for (String str : lines) {
++line;
if (startY < mMiddleY)
canvas.drawText(str, mMiddleX, startY - (linesLength - line) * DY, paint);
else
canvas.drawText(str, mMiddleX, startY + (line - 1) * DY, paint);
}
} else {
++line;
mPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(text, mMiddleX, startY, paint);
}
return line;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (lyric == null)
return;
List<Lyric.Sentence> sentenceList = lyric.sentenceList;
if (sentenceList == null || sentenceList.isEmpty() || mLyricIndex == -2) {
return;
}
canvas.drawColor(0xEFeffff);
float currY;
if (mLyricIndex > -1) {
// Current line with highlighted color
currY = mMiddleY + DY * drawText(
canvas, mCurrentPaint, sentenceList.get(mLyricIndex).content, mMiddleY);
} else {
// First line is not from timestamp 0
currY = mMiddleY + DY;
}
// Draw sentences afterwards
int size = sentenceList.size();
for (int i = mLyricIndex + 1; i < size; i++) {
if (currY > mHeight) {
break;
}
// Draw and Move down
currY += DY * drawText(canvas, mPaint, sentenceList.get(i).content, currY);
// canvas.translate(0, DY);
}
currY = mMiddleY - DY;
// Draw sentences before current one
for (int i = mLyricIndex - 1; i >= 0; i--) {
if (currY < 0) {
break;
}
// Draw and move upwards
currY -= DY * drawText(canvas, mPaint, sentenceList.get(i).content, currY);
// canvas.translate(0, DY);
}
if (mIsTouched > 0) {
mPaint.setTextAlign(Paint.Align.LEFT);
canvas.drawText(String.format("%s - %s", lyric.artist, lyric.title), 10, 50, mPaint);
canvas.drawText("offset: " + lyric.offset, 10, 150, mPaint);
if (mLyricIndex >= 0) {
int seconds = (int) ((lyric.sentenceList.get(mLyricIndex).fromTime / 1000));
int minutes = seconds / 60;
seconds = seconds % 60;
canvas.drawText(String.format("%02d:%02d", minutes, seconds), 10, 100, mPaint);
}
--mIsTouched;
mPaint.setTextAlign(Paint.Align.CENTER);
}
}
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
mMiddleX = w * 0.5f; // remember the center of the screen
mHeight = h;
mMiddleY = h * 0.5f;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
final boolean superResult = super.onTouchEvent(event);
if (lyric == null) {
return superResult;
}
boolean handled = false;
boolean offsetChanged = false;
switch (action) {
case MotionEvent.ACTION_MOVE:
mIsTouched = 3;
float y = event.getY();
if (mLastEffectY != 0) {
if (mLastEffectY - y > 10) {
int times = (int) ((mLastEffectY - y) / 10);
mLastEffectY = y;
lyric.offset += times * -100;
offsetChanged = true;
} else if (mLastEffectY - y < -10) {
int times = -(int) ((mLastEffectY - y) / 10);
mLastEffectY = y;
lyric.offset += times * 100;
offsetChanged = true;
}
}
handled = true;
break;
case MotionEvent.ACTION_DOWN:
handled = true;
mLastEffectY = event.getY();
mIsTouched = 3;
break;
case MotionEvent.ACTION_UP:
System.currentTimeMillis();
mLastEffectY = 0;
handled = true;
break;
default:
break;
}
if (handled) {
if (offsetChanged) {
mIsNeedUpdate = true;
}
return true;
}
return superResult;
}
/**
* @param time Timestamp of current sentence
* @return Timestamp of next sentence, -1 if is last sentence.
*/
public long updateIndex(long time) {
// Current index is last sentence
if (mLyricIndex >= mLyricSentenceLength - 1) {
mLyricIndex = mLyricSentenceLength - 1;
return -1;
}
// Get index of sentence whose timestamp is between its startTime and currentTime.
mLyricIndex = LyricUtils.getSentenceIndex(lyric, time, mLyricIndex, lyric.offset);
// New current index is last sentence
if (mLyricIndex >= mLyricSentenceLength - 1) {
mLyricIndex = mLyricSentenceLength - 1;
return -1;
}
return lyric.sentenceList.get(mLyricIndex + 1).fromTime + lyric.offset;
}
public synchronized void setLyric(Lyric lyric, boolean resetIndex) {
this.lyric = lyric;
mLyricSentenceLength = this.lyric.sentenceList.size();
if (resetIndex) {
mLyricIndex = 0;
}
}
public void setLyricIndex(int index) {
mLyricIndex = index;
}
public String getCurrentSentence() {
if (mLyricIndex >= 0 && mLyricIndex < mLyricSentenceLength) {
return lyric.sentenceList.get(mLyricIndex).content;
}
return null;
}
/**
* Check if view need to update due to user input.
*
* @return Whether need update view.
*/
public boolean checkUpdate() {
if (mIsNeedUpdate) {
mIsNeedUpdate = false;
return true;
}
return false;
}
public synchronized void setLyric(Lyric lyric) {
setLyric(lyric, true);
}
public void play() {
mStop = false;
Thread thread = new Thread(this);
thread.start();
}
public void stop() {
mStop = true;
}
private long mStartTime = -1;
private boolean mStop = true;
private boolean mIsForeground = true;
private long mNextSentenceTime = -1;
private Handler mHandler = new Handler();
@Override
public void run() {
if (mStartTime == -1) {
mStartTime = System.currentTimeMillis();
}
while (mLyricIndex != -2) {
if (mStop) {
return;
}
long ts = System.currentTimeMillis() - mStartTime;
if (ts >= mNextSentenceTime || checkUpdate()) {
mNextSentenceTime = updateIndex(ts);
if (mOnLyricUpdateListener != null) {
mOnLyricUpdateListener.onLyricUpdate();
}
// Redraw only when window is visible
if (mIsForeground) {
mHandler.post(new Runnable() {
@Override
public void run() {
invalidate();
}
});
}
}
if (mNextSentenceTime == -1) {
mStop = true;
}
}
}
public interface OnLyricUpdateListener {
void onLyricUpdate();
}
}