全民k歌大家都不陌生吧,在嗨歌时有一个线谱样式的动画效果是不是很吸引人呢。
效果似乎很复杂,感觉上非自定义view莫属了,然而如何处理滑动、如何处理颜色、如何处理多段线条、如何处理数据变化......等都搞好了准备写的时候才发现————一个星期过去了......?
其实如果把每条线都当做简单的自定义view你会发现:就是一个RecyclerView+一条线而已(由于rv涉及到复用、重绘,当自己自定义时如果使用不当会出现各种问题,对于新手可以使用ScrollView+自定义View的实现方式,这样只要一次性初始化完遇到刷新调用invalidate就行了,不需要复用和重绘,数百个自定义的线只会比rv多5M内存。思路同下,具体实现就相对简单多了,可以自己试试)
思路:一个一直滑动不可拖动的rv+可以变颜色的自定义view
由于代码不算太多(强忍不说)直接贴出成品吧:
public class KGeActivity extends BaseActivity {
@BindView(R.id.fl_KGeXian)
FrameLayout mFl;
@BindView(R.id.view_KGeXian_Xian)
View mViewXian;
RecyclerView mRv;
private BaseAdapterRvList<BaseViewHolder, LineData> mAdapter;
private StartAnimat mAnimat = new StartAnimat();//滑动动画
private int mRvWidths;//rv的总长度,计算得来
private int mViewMargin = 300;//分割线距左边的位置
/**
* 音乐总时长
*/
private int mMusicTime = 100_000;
/**
* 声谱对应控件的信息
*/
private ArrayList<LineData> mList = new ArrayList<>();
@Override
protected int getLayouRes() {//等同于setContentView
return R.layout.activity_k_ge;
}
//等同于onCreate
@Override
protected void initData() {
//由于普通操作无法完全屏蔽事件,此处直接重写rv拦截全部事件
mRv = new RecyclerView(this) {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return true;
}
};
mFl.addView(mRv, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mRv.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
//初始化adapter
mAdapter = new BaseAdapterRvList<BaseViewHolder, LineData>(this) {
//等同于bind
@Override
protected void onBindVH(BaseViewHolder baseViewHolder, int i, LineData lineData) {
XianView xv = (XianView) baseViewHolder.itemView;
xv.setData(lineData);
}
//等同于creat
@NonNull
@Override
protected BaseViewHolder onCreateVH(ViewGroup viewGroup, LayoutInflater layoutInflater) {
XianView xv = new XianView(getActivity());
xv.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
return new BaseViewHolder(xv);
}
};
//添加一个偏移的header
View nullView = new View(this);
nullView.setLayoutParams(new RecyclerView.LayoutParams(mViewMargin, 1));
mAdapter.addHeaderView(nullView);
mRv.setAdapter(mAdapter);
mViewXian.post(new Runnable() {
@Override
public void run() {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mViewXian.getLayoutParams();
params.leftMargin = mViewMargin;
mViewXian.setLayoutParams(params);
}
});
//添加模拟数据
LineData ld1 = new LineData();//开始空白
ld1.lineLength = 1000;
ld1.noData = true;
mList.add(ld1);
for (int i = 0; i < 100; i++) {
LineData xd = new LineData();
xd.lineY = i % 5 * 50;
xd.lineLength = (i % 5 + 1) * 50;
xd.noData = i % 8 == 0;
mList.add(xd);
}
LineData ld2 = new LineData();//结束空白
ld2.lineLength = 1000;
ld2.noData = true;
mList.add(ld2);
mRvWidths = 0;
for (LineData lineData : mList) {
mRvWidths += lineData.lineLength;
}
mAdapter.setListAndNotifyDataSetChanged(mList);//等同于刷新数据
//开启滑动
mAnimat.start();
//随机k歌匹配
suiJi();
}
/**
* 随机生成匹配数据
*/
private void suiJi() {
new Thread() {
@Override
public void run() {
super.run();
while (true) {
try {
sleep((long) (Math.random() * 1000));//模拟匹配失败
} catch (InterruptedException e) {
e.printStackTrace();
}
final long now1 = System.currentTimeMillis();
if (now1 - mAnimat.mStartTime >= mMusicTime) {
return;//结束
}
//模拟匹配成功
final int oneTimeLength = 50;
for (int i = 0; i < (int) (Math.random() * 40) + 20; i++) {
try {
sleep(oneTimeLength);
} catch (InterruptedException e) {
e.printStackTrace();
}
final long now2 = System.currentTimeMillis();
runOnUiThread(new Runnable() {
@Override
public void run() {
successSing(now2, oneTimeLength);//告诉主线程,有匹配成功的数据来了
}
});
}
}
}
}.start();
}
/**
* 用户某段唱成功了
*
* @param endTime 结束时间
* @param timeLength 持续时间
*/
private void successSing(long endTime, int timeLength) {
//唱对的这段在rv的位置=rv总长度*时间比例
int startWidth = (int) (mRvWidths * (endTime - timeLength - mAnimat.mStartTime) / mMusicTime);
int endWidth = (int) (mRvWidths * (endTime - mAnimat.mStartTime) / mMusicTime);
int currentWidth = 0;//当前正在遍历item的起始点
for (int i = 0; i < mList.size(); i++) {
LineData lineData = mList.get(i);
int lineEnd = currentWidth + lineData.lineLength;
if (startWidth >= currentWidth && startWidth < lineEnd) {//相交,成功的在右侧部分或被包含
if (endWidth > lineEnd) {//相交于右侧
addKSizeInfo(lineData.kSizeInfo, startWidth - currentWidth, lineData.lineLength);
} else {//整个被包含
addKSizeInfo(lineData.kSizeInfo, startWidth - currentWidth, endWidth - currentWidth);
}
mAdapter.notifyDataSetChanged();//notifyItemChanged局部刷新有闪动
} else if (currentWidth >= startWidth && currentWidth < endWidth) {//相交,成功的在左侧部分或包含整个
if (lineEnd > endWidth) {//相交于左侧
addKSizeInfo(lineData.kSizeInfo, 0, endWidth - currentWidth);
} else {//包含整段
addKSizeInfo(lineData.kSizeInfo, 0, lineData.lineLength);
}
mAdapter.notifyDataSetChanged();//notifyItemChanged局部刷新有闪动
} else if (endWidth < currentWidth) {//遍历过头了
break;
}
currentWidth = lineEnd;//结束继续下一个循环
}
}
/**
* 合并里面的集合
*/
private void addKSizeInfo(List<int[]> kSizeInfo, int start, int end) {
if (kSizeInfo.size() > 0) {
int[] ints = kSizeInfo.get(kSizeInfo.size() - 1);
if (ints[1] - start >= -1) {//重合就合并成一个
ints[1] = end;
} else {
kSizeInfo.add(new int[]{start, end});
}
} else {
kSizeInfo.add(new int[]{start, end});
}
}
/**
* 根据音乐时间和list数据均匀滑动
*/
class StartAnimat {
private long mStartTime;//启动时间
private int mLastX;//当前滑动的长度
private Runnable mRun = new Runnable() {
@Override
public void run() {
if (isFinishing() || !mRv.canScrollHorizontally(1)) {
Utils.toast("结束");
return;
}
double now = System.currentTimeMillis();
int nowX = (int) (mRvWidths * (now - mStartTime) / mMusicTime);
mRv.scrollBy(nowX - mLastX, 0);//rv只有by
mLastX = nowX;
ViewCompat.postOnAnimation(mRv, mRun);//循环移动
}
};
/**
* 开启滑动
*/
public void start() {
mStartTime = System.currentTimeMillis();
mLastX = 0;
ViewCompat.postOnAnimation(mRv, mRun);
}
}
@Override
protected void setListener() {
}
public static class XianView extends View implements ViewInterface {
//线高
private int mLineHeight = 10;
private Paint mPaint;
private LineData mData;
public XianView(Context context) {
this(context, null, 0);
}
public XianView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public XianView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initData();
initAttrs(attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public XianView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initData();
initAttrs(attrs);
}
@Override
public void initData() {
//此处要inflate,不需要可以删掉黄油刀
ButterKnife.bind(this);//注册黄油刀
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setStrokeWidth(mLineHeight);
}
/**
* 简单使用,高度直接写死,具体多高自行判断
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mData != null && mData.lineLength > 0) {//有宽度直接设置
super.onMeasure(MeasureSpec.makeMeasureSpec(mData.lineLength, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(Utils.dip2px(getContext(), 100), MeasureSpec.EXACTLY));
} else {//没宽度直接是0
super.onMeasure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(Utils.dip2px(getContext(), 100), MeasureSpec.EXACTLY));
}
}
@Override
public void initAttrs(@Nullable AttributeSet attrs) {
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mData == null || mData.noData) {
return;
}
mPaint.setColor(0xffdddddd);//灰
int h_2 = mLineHeight / 2;
canvas.drawLine(h_2, mData.lineY + h_2, mData.lineLength + h_2, mData.lineY + h_2, mPaint);
if (mData.kSizeInfo != null) {
mPaint.setColor(0xffff00ff);//红
for (int[] kSize : mData.kSizeInfo) {
canvas.drawLine(kSize[0] + h_2, mData.lineY + h_2, kSize[1] + h_2, mData.lineY + h_2, mPaint);
}
}
}
/**
* 设置或刷新数据
*/
public void setData(LineData data) {
if (mData != null && mData.lineLength == data.lineLength) {
//宽度不变只需要重绘即可
mData = data;
invalidate();
} else {
//宽度改变需要重新加载布局
mData = data;
requestLayout();
}
}
/**
* 设置线的厚度
*/
public void setLineHeight(int heightPx) {
mLineHeight = heightPx;
mPaint.setStrokeWidth(mLineHeight);
}
}
public static class LineData {
/**
* 线距上的距离
*/
public int lineY;
/**
* 线的最大长度
*/
public int lineLength;
/**
* 用户k歌时匹配正确的信息{开始位置,结束位置}
*/
public List<int[]> kSizeInfo = new ArrayList<>();
/**
* 空白数据,不需要唱时为true,{@link #lineLength}为等待的长度
*/
public boolean noData = false;
}
}
使用的adapter见这篇:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/c_fff"
android:gravity="center_horizontal"
android:orientation="vertical">
<FrameLayout
android:id="@+id/fl_KGeXian"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--margin代码修改-->
<View
android:id="@+id/view_KGeXian_Xian"
android:layout_width="1dp"
android:layout_height="100dp"
android:layout_marginLeft="1dp"
android:background="@color/c_w_333"
/>
</FrameLayout>
</LinearLayout>
效果图
是不是感觉很简单呢?(刚填完黑洞的博主轻松的向大家挥手)
坑已经帮大家填完了,具体暂停操作、数据处理等细节就自行解决吧