package com.wm.remusic.lrc;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;
import com.wm.remusic.MainApplication;
import com.wm.remusic.R;
import com.wm.remusic.uitl.CommonUtils;
import java.util.List;
/***
* 须知:
* 在ViewGroup里面 scrollTo,scrollBy方法移动的是子View
* 在View里面scrollTo,scrollBy方法移动的是View里面绘制的内容
* 要点:
* 1:歌词的上下平移用什么实现?
* 用Scroller实现,Scroller只是一个工具而已,
* 真正实现滚动效果的还是View的scrollTo方法
* 2:歌词的水平滚动怎么实现?
* 通过属性动画ValueAnimator控制高亮歌词绘制的x轴起始坐标
*
* @author Ligang 2014/8/19
*/
public class LrcView extends View implements ILrcView {
/**
* 所有的歌词
***/
private List<LrcRow> mLrcRows;
/**
* 无歌词数据的时候 显示的默认文字
**/
private static final String DEFAULT_TEXT = "暂时没有歌词";
/**
* 默认文字的字体大小
**/
private static final float SIZE_FOR_DEFAULT_TEXT = CommonUtils.dip2px(MainApplication.context,17);
/**
* 画高亮歌词的画笔
***/
private Paint mPaintForHighLightLrc;
/**
* 高亮歌词的默认字体大小
***/
private static final float DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC = CommonUtils.dip2px(MainApplication.context,15);
/**
* 高亮歌词当前的字体大小
***/
private float mCurSizeForHightLightLrc = DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC;
/**
* 高亮歌词的默认字体颜色
**/
private static final int DEFAULT_COLOR_FOR_HIGHT_LIGHT_LRC = 0xffffffff;
/**
* 高亮歌词当前的字体颜色
**/
private int mCurColorForHightLightLrc = DEFAULT_COLOR_FOR_HIGHT_LIGHT_LRC;
/**
* 画其他歌词的画笔
***/
private Paint mPaintForOtherLrc;
/**
* 其他歌词的默认字体大小
***/
private static final float DEFAULT_SIZE_FOR_OTHER_LRC = CommonUtils.dip2px(MainApplication.context,15);
/**
* 其他歌词当前的字体大小
***/
private float mCurSizeForOtherLrc = DEFAULT_SIZE_FOR_OTHER_LRC;
/**
* 其他歌词的默认字体颜色
**/
private static final int DEFAULT_COLOR_FOR_OTHER_LRC = 0x80ffffff;
/**
* 其他歌词当前的字体颜色
**/
private int mCurColorForOtherLrc = DEFAULT_COLOR_FOR_OTHER_LRC;
/**
* 画时间线的画笔
***/
private Paint mPaintForTimeLine;
/***
* 时间线的颜色
**/
private static final int COLOR_FOR_TIME_LINE = 0xff999999;
/**
* 时间文字大小
**/
private static final int SIZE_FOR_TIME = CommonUtils.dip2px(MainApplication.context,12);
/**
* 是否画时间线
**/
private boolean mIsDrawTimeLine = false;
/**
* 歌词间默认的行距
**/
private static final float DEFAULT_PADDING = CommonUtils.dip2px(MainApplication.context,17);
/**
* 歌词当前的行距
**/
private float mCurPadding = DEFAULT_PADDING;
/**
* 歌词的最大缩放比例
**/
public static final float MAX_SCALING_FACTOR = 1.5f;
/**
* 歌词的最小缩放比例
**/
public static final float MIN_SCALING_FACTOR = 0.5f;
/**
* 默认缩放比例
**/
private static final float DEFAULT_SCALING_FACTOR = 1.0f;
/**
* 歌词的当前缩放比例
**/
private float mCurScalingFactor = DEFAULT_SCALING_FACTOR;
/**
* 实现歌词竖直方向平滑滚动的辅助对象
**/
private Scroller mScroller;
/***
* 移动一句歌词的持续时间
**/
private static final int DURATION_FOR_LRC_SCROLL = 1500;
/***
* 停止触摸时 如果View需要滚动 时的持续时间
**/
private static final int DURATION_FOR_ACTION_UP = 400;
/**
* 控制文字缩放的因子
**/
private float mCurFraction = 0;
private int mTouchSlop;
private Bitmap arrowBitmap;
public LrcView(Context context) {
super(context);
init(context);
}
public LrcView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
/**
* 初始化画笔等
*/
@Override
public void init(Context context) {
mScroller = new Scroller(getContext());
mPaintForHighLightLrc = new Paint();
mPaintForHighLightLrc.setColor(mCurColorForHightLightLrc);
mPaintForHighLightLrc.setTextSize(mCurSizeForHightLightLrc);
mPaintForHighLightLrc.setAntiAlias(true);
mPaintForOtherLrc = new Paint();
mPaintForOtherLrc.setColor(mCurColorForOtherLrc);
mPaintForOtherLrc.setTextSize(mCurSizeForOtherLrc);
mPaintForOtherLrc.setAntiAlias(true);
mPaintForTimeLine = new Paint();
mPaintForTimeLine.setColor(COLOR_FOR_TIME_LINE);
mPaintForTimeLine.setTextSize(SIZE_FOR_TIME);
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = 30;
options.inTargetDensity = 30;
arrowBitmap = BitmapFactory.decodeResource(context.getResources(), R.raw.lrc_arrow, options);
}
private int mTotleDrawRow;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mLrcRows == null || mLrcRows.size() == 0) {
//画默认的显示文字
mPaintForOtherLrc.setTextSize(SIZE_FOR_DEFAULT_TEXT);
float textWidth = mPaintForOtherLrc.measureText(DEFAULT_TEXT);
float textX = (getWidth() - textWidth) / 2;
canvas.drawText(DEFAULT_TEXT, textX, getHeight() / 2, mPaintForOtherLrc);
return;
}
if (mTotleDrawRow == 0) {
//初始化将要绘制的歌词行数
mTotleDrawRow = (int) (getHeight() / (mCurSizeForOtherLrc + mCurPadding)) + 4;
}
//因为不需要将所有歌词画出来
int minRaw = mCurRow - (mTotleDrawRow - 1) / 2;
int maxRaw = mCurRow + (mTotleDrawRow - 1) / 2;
minRaw = Math.max(minRaw, 0); //处理上边界
maxRaw = Math.min(maxRaw, mLrcRows.size() - 1); //处理下边界
//实现渐变的最大歌词行数
int count = Math.max(maxRaw - mCurRow, mCurRow - minRaw);
if (count == 0) {
return;
}
//两行歌词间字体颜色变化的透明度
int alpha = (0xFF - 0x11) / count;
//画出来的第一行歌词的y坐标
float rowY = getHeight() / 2 + minRaw * (mCurSizeForOtherLrc + mCurPadding);
for (int i = minRaw; i <= maxRaw; i++) {
if (i == mCurRow) {//画高亮歌词
//因为有缩放效果,所有需要动态设置歌词的字体大小
float textSize = mCurSizeForOtherLrc + (mCurSizeForHightLightLrc - mCurSizeForOtherLrc) * mCurFraction;
mPaintForHighLightLrc.setTextSize(textSize);
String text = mLrcRows.get(i).getContent();//获取到高亮歌词
float textWidth = mPaintForHighLightLrc.measureText(text);//用画笔测量歌词的宽度
if (textWidth > getWidth()) {
//如果歌词宽度大于view的宽,则需要动态设置歌词的起始x坐标,以实现水平滚动
canvas.drawText(text, mCurTextXForHighLightLrc, rowY, mPaintForHighLightLrc);
} else {
//如果歌词宽度小于view的宽,则让歌词居中显示
float textX = (getWidth() - textWidth) / 2;
canvas.drawText(text, textX, rowY, mPaintForHighLightLrc);
}
} else {
if (i == mLastRow) {//画高亮歌词的上一句
//因为有缩放效果,所有需要动态设置歌词的字体大小
float textSize = mCurSizeForHightLightLrc - (mCurSizeForHightLightLrc - mCurSizeForOtherLrc) * mCurFraction;
mPaintForOtherLrc.setTextSize(textSize);
} else {//画其他的歌词
mPaintForOtherLrc.setTextSize(mCurSizeForOtherLrc);
}
String text = mLrcRows.get(i).getContent();
float textWidth = mPaintForOtherLrc.measureText(text);
float textX = (getWidth() - textWidth) / 2;
//如果计算出的textX为负数,将textX置为0(实现:如果歌词宽大于view宽,则居左显示,否则居中显示)
textX = Math.max(textX, 0);
//实现颜色渐变 从0xFFFFFFFF 逐渐变为 0x11FFFFFF(颜色还是白色,只是透明度变化)
int curAlpha = 255 - (Math.abs(i - mCurRow) - 1) * alpha; //求出当前歌词颜色的透明度
//mPaintForOtherLrc.setColor(0x1000000*curAlpha+0xffffff);
canvas.drawText(text, textX, rowY, mPaintForOtherLrc);
}
//计算出下一行歌词绘制的y坐标
rowY += mCurSizeForOtherLrc + mCurPadding;
}
//画时间线和时间
if (mIsDrawTimeLine) {
float y = getHeight() / 2 + getScrollY();
float x = getWidth();
canvas.drawBitmap(arrowBitmap, -20, y - 41, null);
canvas.drawText(mLrcRows.get(mCurRow).getTimeStr().substring(0, 5), x - 105, y + 13, mPaintForTimeLine);
canvas.drawLine(60, y, getWidth() - 110, y, mPaintForTimeLine);
}
}
/**
* 是否可拖动歌词
**/
private boolean canDrag = false;
/**
* 事件的第一次的y坐标
**/
private float firstY;
/**
* 事件的上一次的y坐标
**/
private float lastY;
private float lastX;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mLrcRows == null || mLrcRows.size() == 0) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
firstY = event.getRawY();
lastX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
if (!canDrag) {
if (Math.abs(event.getRawY() - firstY) > mTouchSlop && Math.abs(event.getRawY() - firstY) > Math.abs(event.getRawX() - lastX)) {
canDrag = true;
mIsDrawTimeLine = true;
mScroller.forceFinished(true);
stopScrollLrc();
mCurFraction = 1;
}
lastY = event.getRawY();
}
if (canDrag) {
float offset = event.getRawY() - lastY;//偏移量
if (getScrollY() - offset < 0) {
if (offset > 0) {
offset = offset / 3;
}
} else if (getScrollY() - offset > mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding) {
if (offset < 0) {
offset = offset / 3;
}
}
scrollBy(getScrollX(), -(int) offset);
lastY = event.getRawY();
int currentRow = (int) (getScrollY() / (mCurSizeForOtherLrc + mCurPadding));
currentRow = Math.min(currentRow, mLrcRows.size() - 1);
currentRow = Math.max(currentRow, 0);
seekTo(mLrcRows.get(currentRow).getTime(), false, false);
return true;
}
lastY = event.getRawY();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (!canDrag) {
if (onLrcClickListener != null) {
onLrcClickListener.onClick();
}
} else {
if (onSeekToListener != null && mCurRow != -1) {
onSeekToListener.onSeekTo(mLrcRows.get(mCurRow).getTime());
}
if (getScrollY() < 0) {
smoothScrollTo(0, DURATION_FOR_ACTION_UP);
} else if (getScrollY() > mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding) {
smoothScrollTo((int) (mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding), DURATION_FOR_ACTION_UP);
}
canDrag = false;
mIsDrawTimeLine = false;
invalidate();
}
break;
}
return true;
}
/**
* 为LrcView设置歌词List集合数据
*/
@Override
public void setLrcRows(List<LrcRow> lrcRows) {
reset();
this.mLrcRows = lrcRows;
invalidate();
}
/**
* 当前高亮歌词的行号
**/
private int mCurRow = -1;
/**
* 上一次的高亮歌词的行号
**/
private int mLastRow = -1;
@Override
public void seekTo(int progress, boolean fromSeekBar, boolean fromSeekBarByUser) {
if (mLrcRows == null || mLrcRows.size() == 0) {
return;
}
//如果是由seekbar的进度改变触发 并且这时候处于拖动状态,则返回
if (fromSeekBar && canDrag) {
return;
}
for (int i = mLrcRows.size() - 1; i >= 0; i--) {
if (progress >= mLrcRows.get(i).getTime()) {
if (mCurRow != i) {
mLastRow = mCurRow;
mCurRow = i;
log("mCurRow=i=" + mCurRow);
if (fromSeekBarByUser) {
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
scrollTo(getScrollX(), (int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding)));
} else {
smoothScrollTo((int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding)), DURATION_FOR_LRC_SCROLL);
}
//如果高亮歌词的宽度大于View的宽,就需要开启属性动画,让它水平滚动
float textWidth = mPaintForHighLightLrc.measureText(mLrcRows.get(mCurRow).getContent());
log("textWidth=" + textWidth + "getWidth()=" + getWidth());
if (textWidth > getWidth()) {
if (fromSeekBarByUser) {
mScroller.forceFinished(true);
}
log("开始水平滚动歌词:" + mLrcRows.get(mCurRow).getContent());
startScrollLrc(getWidth() - textWidth, (long) (mLrcRows.get(mCurRow).getTotalTime() * 0.6));
}
invalidate();
}
break;
}
}
}
/**
* 控制歌词水平滚动的属性动画
***/
private ValueAnimator mAnimator;
/**
* 开始水平滚动歌词
*
* @param endX 歌词第一个字的最终的x坐标
* @param duration 滚动的持续时间
*/
private void startScrollLrc(float endX, long duration) {
if (mAnimator == null) {
mAnimator = ValueAnimator.ofFloat(0, endX);
mAnimator.addUpdateListener(updateListener);
} else {
mCurTextXForHighLightLrc = 0;
mAnimator.cancel();
mAnimator.setFloatValues(0, endX);
}
mAnimator.setDuration(duration);
mAnimator.setStartDelay((long) (duration * 0.3)); //延迟执行属性动画
mAnimator.start();
}
/**
* 停止歌词的滚动
*/
private void stopScrollLrc() {
if (mAnimator != null) {
mAnimator.cancel();
}
mCurTextXForHighLightLrc = 0;
}
/**
* 高亮歌词当前的其实x轴绘制坐标
**/
private float mCurTextXForHighLightLrc;
/***
* 监听属性动画的数值值的改变
*/
AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//TODO
mCurTextXForHighLightLrc = (Float) animation.getAnimatedValue();
log("mCurTextXForHighLightLrc=" + mCurTextXForHighLightLrc);
invalidate();
}
};
/**
* 设置歌词的缩放比例
*/
@Override
public void setLrcScalingFactor(float scalingFactor) {
mCurScalingFactor = scalingFactor;
mCurSizeForHightLightLrc = DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC * mCurScalingFactor;
mCurSizeForOtherLrc = DEFAULT_SIZE_FOR_OTHER_LRC * mCurScalingFactor;
mCurPadding = DEFAULT_PADDING * mCurScalingFactor;
mTotleDrawRow = (int) (getHeight() / (mCurSizeForOtherLrc + mCurPadding)) + 3;
log("mRowTotal=" + mTotleDrawRow);
scrollTo(getScrollX(), (int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding)));
invalidate();
mScroller.forceFinished(true);
}
/**
* 重置
*/
@Override
public void reset() {
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
mLrcRows = null;
scrollTo(getScrollX(), 0);
invalidate();
}
/**
* 平滑的移动到某处
*
* @param dstY
*/
private void smoothScrollTo(int dstY, int duration) {
int oldScrollY = getScrollY();
int offset = dstY - oldScrollY;
mScroller.startScroll(getScrollX(), oldScrollY, getScrollX(), offset, duration);
invalidate();
}
@Override
public void computeScroll() {
if (!mScroller.isFinished()) {
if (mScroller.computeScrollOffset()) {
int oldY = getScrollY();
int y = mScroller.getCurrY();
if (oldY != y && !canDrag) {
scrollTo(getScrollX(), y);
}
mCurFraction = mScroller.timePassed() * 3f / DURATION_FOR_LRC_SCROLL;
mCurFraction = Math.min(mCurFraction, 1F);
invalidate();
}
}
}
/**
* 返回当前的歌词缩放比例
*
* @return
*/
public float getmCurScalingFactor() {
return mCurScalingFactor;
}
private OnSeekToListener onSeekToListener;
public void setOnSeekToListener(OnSeekToListener onSeekToListener) {
this.onSeekToListener = onSeekToListener;
}
public interface OnSeekToListener {
void onSeekTo(int progress);
}
private OnLrcClickListener onLrcClickListener;
public void setOnLrcClickListener(OnLrcClickListener onLrcClickListener) {
this.onLrcClickListener = onLrcClickListener;
}
public interface OnLrcClickListener {
void onClick();
}
public void log(Object o) {
Log.d("LrcView", o + "");
}
}