Android - 自定义View 实现 文本吉他谱的 动态控制 实现

本文介绍了一种针对吉他谱的显示与控制方法,重点攻克了文本吉他谱的解析、动态控制、显示优化等问题,并提供了具体的实现思路和技术细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.背景

     前天同学推荐,要做一个吉他谱的显示,与控制功能;想了想,感觉难度有些大,主要有几个难点:

    难点1 :显示:文本吉他谱是txt ,需要 和弦键与对应的文字对应显示出来;

    难点2: 控制:需要通过音量键进行动态的控制,+ ,向下控制当前的和弦键,-,向上控制当前的和弦键;(完成)

    难点3: 显示:当前控制的和弦键需要放大和变颜色,和当前行歌词颜色,正常行歌词颜色,均不一样;

    难点4: 控制:通过和弦键来控制整体歌词的滚动;(完成)

    难点5: 回调:需要回调 当前正在使用的和弦键,和下一个和弦键;(完成)


2. 动态控制思路

    2.1 普通歌词思路

          正常情况下,歌词的控制是通过时间作为切入点进行控制,根据时间的毫秒数与当前播放的时间大小,进行判断,可以控制其播放,暂停,和播放位置的设置;

          这是之前写的效果实现:

http://blog.youkuaiyun.com/lablenet/article/details/50328987

          而文本吉他谱,比如《都是你》的吉他谱的歌谱:

都是你
光良
原 调:1=#F
选 调:1=C
变调夹:6品
 C             F      Dm7   G      C
*谁 改变了我的世界 没有方向 没有日夜
  F         C Em/B Am      Dm           G
我看著天 这一刻在想你 是否会对我一样思念
  C            F        Dm7   G       C
#你 曾说我们有一个梦 等到那天 我们来实现
  F         C Em/B Am      Dm   G       C
我望著天 在心中默默念 下一秒你出现在眼前
       F    G        C
想念的心 装满的都是你
       F    G        C
我的钢琴 弹奏的都是你
       F    G       C Em/B Am
我的日记 写满的都是你的名
     F     G       C
才发现 又另一个黎明
       F     G      C Em/B Am
我的日记 写满的都是你的名
     F     G       C
才发现 又是一个黎明
     F     G       C
这是我 对你爱的累积

     上面的英文字母是和弦键,下面的中文为其这首歌的歌词,相比普通的LRC歌词就麻烦对了;

     下面分享下我解决的思路 :

  2.2 吉他谱歌词思路

        首先是切入点,肯定是和弦键了,通过和弦键的控制,进而对整个歌谱的控制;

          

        实现步骤 :

               (1)读取歌谱 ,将 和弦键 所在行 存储在 List1中(没有和弦的存储为no),歌词存储在 List2中;

                        比如:

public void loadLrc(String lrcName) throws Exception {
		mLrcTexts.clear();
		mLrcKeys.clear();
		BufferedReader br = new BufferedReader(new InputStreamReader(
				getResources().getAssets().open(lrcName)));
		String line;
		int index = 0;
		while ((line = br.readLine()) != null) {
			// 单行加载解析
			// 1.判断最后是否有 $ 符号
			index = line.lastIndexOf("$");
			if (index > 0) {
				// 则此行为 和弦行,进行解析,存储下一行数据

				// 存储和弦值
				line = line.substring(0, index);
				mLrcKeys.add(line);
				// 存储对应的歌词
				line = br.readLine();
				if (line != null) {
					mLrcTexts.add(line);
				} else {
					break;
				}

			} else {
				// 没有$符号,存储歌词,存储和弦为no
				mLrcKeys.add(MSG_KEY_NO);
				mLrcTexts.add(line);
			}

		}
		br.close();
		// 记录总行数
		rowNums = mLrcTexts.size();

		for (int i = 0; i < mLrcKeys.size() - 1; i++) {
			Log.d("Lrc", mLrcKeys.get(i));
		}
		Log.d("Lrc", " mLrcKeys : " + mLrcKeys.size());
		Log.d("Lrc", " mLrcTexts : " + mLrcTexts.size());
	}


               (2)普通显示:通过重写 onDraw() 方法,将文字显示在view上,没有经过处理;如果为no , 则不显示

                        注意:在drawText的时候,需要将和弦行放在歌词上面,则使得其y坐标,错开50 ,而坐标相等,

                  

@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		if (mLrcKeys.isEmpty() || mLrcTexts.isEmpty()) {
			return;
		}

		// 中心Y坐标
		float centerY = getHeight() / 2 + mTextSize / 2 + mAnimOffset;
		// mCurrentLine=5;
		// 画当前行
		String currStr = mLrcTexts.get(mCurrentLine);
		String currKey = mLrcKeys.get(mCurrentLine);
		float currX = (getWidth() - mCurrentPaint.measureText(currStr)) / 2;
		if (!MSG_KEY_NO.equals(currKey)) {
			canvas.drawText(currKey, currX, centerY - 50, mCurrentPaint);
		}
		canvas.drawText(currStr, currX, centerY, mCurrentPaint);

		// 画当前行上面的
		for (int i = mCurrentLine - 1; i >= 0; i--) {
			String upStr = mLrcTexts.get(i);
			String upKey = mLrcKeys.get(i);

			float upX = (getWidth() - mNormalPaint.measureText(upStr)) / 2;
			float upY = centerY - (mTextSize + mDividerHeight)
					* (mCurrentLine - i);
			if (!MSG_KEY_NO.equals(upKey)) {
				canvas.drawText(upKey, upX, upY - 50, mNormalPaint);
			}
			canvas.drawText(upStr, upX, upY, mNormalPaint);
		}

		// 画当前行下面的
		for (int i = mCurrentLine + 1; i < mLrcKeys.size(); i++) {
			String downStr = mLrcTexts.get(i);
			String downKey = mLrcKeys.get(i);
			float downX = (getWidth() - mNormalPaint.measureText(downStr)) / 2;
			float downY = centerY + (mTextSize + mDividerHeight)
					* (i - mCurrentLine);
			if (!MSG_KEY_NO.equals(downKey)) {
				canvas.drawText(downKey, downX, downY - 50, mNormalPaint);
			}
			canvas.drawText(downStr, downX, downY, mNormalPaint);
		}
	}


       (3)控制实现

                基本思路:通过和弦键数组进行判断和切换行;

                1) 给其一个计数器chrodsIndex,也是每行和弦数组的的下标:

                   chrodsIndex=-1 , 则代表这一行没有和弦键 ,切换下一行;

                   chrodsIndex >-1 ,则代表有和弦键;

                2)有和弦,进行解析这一行,获得和弦数组,通过改变 chrodsIndex的值,进行控制

                     临界值1 :数组上边界,切换到下一行,同时读取这一行的和弦键;

                     临界值2:数组下边界,切换到上一行,同时读取这一行的和弦键;

               3)控制输入 x : 1 ,切换和弦向下,-1,切换和弦向上;

                     

    (4)解析实现

	/**
	 * 1. 解析当前行的和弦
	 */
	private void parseLine(int isNext) {
		if (isNext > 0) {
			mNext++;
		} else {
			mNext--;
		}
		mCurrentLine = mNext == rowNums ? 0 : mNext;
		mCurrentLine = mNext < 1 ? 0 : mNext;
		mNext = mCurrentLine;
		chords = null;
		// 获得当前的和弦
		String key = mLrcKeys.get(mCurrentLine);
		// 解析
		if (MSG_KEY_NO.equals(key)) {
			chordsIndex = -1;
			// 没有和弦,更新下一行
			mHandler.sendEmptyMessage(MSG_NEW_LINE);
		} else {
			// 有和弦
			chords = key.trim().split("\\s+");
			// 测试
			String t = " ";
			for (int i = 0; i < chords.length; i++) {
				t += chords[i] + " ";
			}

			Log.d("Lrc", "和弦 :" + t);
			Log.d("Lrc", "歌词 :" + mLrcTexts.get(mCurrentLine));
			// 如果进行下一行,则
			if (isNext < 0) {
				chordsIndex = chords.length;
			} else {
				chordsIndex = 0;
			}
			mHandler.sendEmptyMessage(MSG_NEW_LINE);
		}

	}


    (5)获得下一个和弦

// 3. 获得下一个和弦值
	/*
	 * 基本思路:
	 * 
	 * 根据当前和弦键的下标+1=下一个和弦下标, 如果下标==数组长度, 则向下循环遍历 下面的和弦,取得最近的和弦返回即可
	 * 
	 * !=数组长度,返回即可
	 */
	public void getNextChord() {
		if (nextChordsIndex == -1) {
			return;
		}
		if (chords != null) {
			if (nextChordsIndex >= chords.length) {
				// 当前行+1
				nextCurrentRow = mCurrentLine + 1;

				for (int i = nextCurrentRow; i < rowNums; i++) {
					String lrcKey = mLrcKeys.get(i);
					if (lrcKey.equals(MSG_KEY_NO)) {
						continue;
					} else {
						// 有和弦的行,取得第一个返回
						nextChord = lrcKey.trim().split("\\s+")[0];
						break;
					}
				}

			} else {
				if (nextChordsIndex > 0) {
					nextChord = chords[nextChordsIndex];
				}
			}
		}
	}


   (6)控制实现

/**
	 * 基本思路: 内部计数器,总行数; 1. x=1 ,进行下一个和弦的更新
	 * 
	 * 2.x=-1,进行上一个和旋的更新 3. 先得到 当前行的所有和弦, 如果有和弦
	 * :将和弦进行解析,后有一个计数器,进行计数;当计数器没有等于和弦总个数的时候,进行下一行显示 如果没有和弦,直接进行下一行显示
	 * 下一行显示之前,先进行和弦判断
	 */
	// 和弦计数器
	private int chordsIndex = -1;
	// 当前的和弦键
	private String nowChord = MSG_KEY_NO;
	// 记录上一次执行的操作
	private int preX = 0;
	// 下一个和弦键的下标
	private int nextChordsIndex = chordsIndex;
	// 当前行
	private int nextCurrentRow = mCurrentLine;
	// 下一个和弦值
	private String nextChord = MSG_KEY_NO;

	/**
	 * 2. 当一行的和弦执行全部切换完毕的时候,进行下一行切换
	 * 
	 * @param x
	 */
	public synchronized void updateLrc(int x) {
		// 最后一行了,不在进行更新操作
		if (x > 0) {
			if (mNext >= rowNums - 1) {
				lrcViewInterface.isPlayToEnd(true);
				return;
			}
		}

		Log.d("Lrc", "chordsIndex 1: " + chordsIndex);

		if (chordsIndex == -1) {
			// 如果chordsIndex等于-1 ,代表着 这一行没有 和弦,则进行滚动到下一行
			// x是1还是-1,如果是1,则代表向下一行,是-1则代表向上一行;
			parseLine(x);
		}

		Log.d("Lrc", "chordsIndex 2: " + chordsIndex);

		if (chords != null) {

			// 判断当前执行的动作和上一次执行的动作是否一样,
			// 如果一样,不进行chordsIndex操作,如果不一样,分别对其进行不同的操作
			if (preX == -1 && x == 1 && chordsIndex != 0) {
				chordsIndex += 1;
			} else if (preX == 1 && x == -1) {
				chordsIndex -= 1;
			}

			// 不等于空,开始控制
			switch (x) {
			case 1:
				// 下一个和弦的切换,更新界面,不更新行
				// 最后一个和弦
				if (chords.length == chordsIndex) {
					// 临界值为最大的时候,切换下一行
					parseLine(1);
					if (chordsIndex > -1) {
						nowChord = chords[chordsIndex];
						Log.d("Lrc", "和弦 +1 if: " + nowChord);
						chordsIndex++;
					}
				} else {
					nowChord = chords[chordsIndex];
					Log.d("Lrc", "和弦 +1 else : " + nowChord);
					chordsIndex++;
				}

				Log.d("Lrc", "chordsIndex 3 : " + chordsIndex);

				break;
			case -1:

				chordsIndex--;
				chordsIndex = chordsIndex < -1 ? -1 : chordsIndex;
				// 上一个和弦的切换,更新界面,不更新行
				// Log.d("Lrc", "和弦 -1 执行了");

				Log.d("Lrc", "chordsIndex -3: " + chordsIndex);

				if (chordsIndex == -1) {
					// 临界值为最小的时候,切换上一行
					parseLine(-1);
					if (chordsIndex > -1) {
						chordsIndex--;
						nowChord = chords[chordsIndex];
						Log.d("Lrc", "和弦 if -1 : " + nowChord);
					}
				} else {
					nowChord = chords[chordsIndex];
					Log.d("Lrc", "和弦 else -1 : " + nowChord);
				}

				Log.d("Lrc", "chordsIndex -4: " + chordsIndex);

				break;
			}

		} else {
			Log.d("Lrc", "和弦数组chords 为空了: ");
		}
		preX = x;
		// 获得下一个和弦键
		nextChordsIndex = x > 0 ? chordsIndex : chordsIndex + 1;
		getNextChord();
		// 和弦回调
		lrcViewInterface.getNowChordAndNextChord(nowChord, nextChord);
	}


     (7)回调实现

              回调通过接口实现回调,这里就不多说了;


3.效果预览

 


4.Demo 下载

   http://download.youkuaiyun.com/detail/lablenet/9404201

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值