android 仿全民k歌 线谱乐谱音高图

全民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见这篇:https://blog.csdn.net/weimingjue/article/details/88190755

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>

效果图

 是不是感觉很简单呢?(刚填完黑洞的博主轻松的向大家挥手)

坑已经帮大家填完了,具体暂停操作、数据处理等细节就自行解决吧

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值