全民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<>();@Overrideprotected int getLayouRes() {//等同于setContentViewreturn R.layout.activity_k_ge;}//等同于onCreate@Overrideprotected void initData() {//由于普通操作无法完全屏蔽事件,此处直接重写rv拦截全部事件mRv = new RecyclerView(this) {@Overridepublic 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));//初始化adaptermAdapter = new BaseAdapterRvList<BaseViewHolder, LineData>(this) {//等同于bind@Overrideprotected void onBindVH(BaseViewHolder baseViewHolder, int i, LineData lineData) {XianView xv = (XianView) baseViewHolder.itemView;xv.setData(lineData);}//等同于creat@NonNull@Overrideprotected 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);}};//添加一个偏移的headerView nullView = new View(this);nullView.setLayoutParams(new RecyclerView.LayoutParams(mViewMargin, 1));mAdapter.addHeaderView(nullView);mRv.setAdapter(mAdapter);mViewXian.post(new Runnable() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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只有bymLastX = nowX;ViewCompat.postOnAnimation(mRv, mRun);//循环移动}};/*** 开启滑动*/public void start() {mStartTime = System.currentTimeMillis();mLastX = 0;ViewCompat.postOnAnimation(mRv, mRun);}}@Overrideprotected 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);}@Overridepublic void initData() {//此处要inflate,不需要可以删掉黄油刀ButterKnife.bind(this);//注册黄油刀mPaint = new Paint();mPaint.setStyle(Paint.Style.FILL_AND_STROKE);mPaint.setStrokeWidth(mLineHeight);}/*** 简单使用,高度直接写死,具体多高自行判断*/@Overrideprotected 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 {//没宽度直接是0super.onMeasure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(Utils.dip2px(getContext(), 100), MeasureSpec.EXACTLY));}}@Overridepublic void initAttrs(@Nullable AttributeSet attrs) {}@Overrideprotected 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见这篇:https://blog.csdn.net/weimingjue/article/details/88190755
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns: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"><FrameLayoutandroid:id="@+id/fl_KGeXian"android:layout_width="match_parent"android:layout_height="match_parent"><!--margin代码修改--><Viewandroid: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>
效果图
是不是感觉很简单呢?(刚填完黑洞的博主轻松的向大家挥手)
坑已经帮大家填完了,具体暂停操作、数据处理等细节就自行解决吧