android音乐搜索功能实现,撸个应用学Android——空灵音乐本地音乐版

前言

第一次写技术博客,首发安卓巴士。使用Kotlin实现一个简易的本地音乐客户端,部分代码为Java语言,Kotlin可以完美调用Java,Enjoy it!

AAffA0nNPuCLAAAAAElFTkSuQmCC

三方库Glide

Anko

BaseRecyclerViewAdapterHelper

Greendao

环境准备

项目使用的是最新的 Android Studio3.0 版本,新版的Studio只需要在项目新建时勾选“Include Kotlin support”即可导入Kotlin的支持,无需配置复杂环境。

效果图

AAffA0nNPuCLAAAAAElFTkSuQmCC

项目目录

AAffA0nNPuCLAAAAAElFTkSuQmCC

歌词显示

歌词显示主要实现逻辑件位于view文件夹下的ILrcBuilder、ILrcView、LrcRow、LrcView四个文件。在需要实现歌词显示的Activity中添加如下布局文件即可:

android:id="@+id/lrcView"

android:layout_width="match_parent"

android:layout_height="0dip"

android:layout_weight="1" />

其中LrcRow文件下可以设置歌词的相关属性参数,例如歌词文字的大小、行间距、正在播放歌词的颜色等等。private List mLrcRows;     // all lrc rows of one lrc file

private int mMinSeekFiredOffset = 10; // min offset for fire seek action, px;

private int mHignlightRow = 0;   // current singing row , should be highlighted.

private int mHignlightRowColor = Color.YELLOW;

private int mNormalRowColor = Color.WHITE;    private int mSeekLineColor = Color.CYAN;    private int mSeekLineTextColor = Color.CYAN;    private int mSeekLineTextSize = 20;    private int mMinSeekLineTextSize = 30;    private int mMaxSeekLineTextSize = 50;    private int mLrcFontSize = 50;     // font size of lrc

private int mMinLrcFontSize = 10;    private int mMaxLrcFontSize = 40;    private int mPaddingY = 43;      // padding of each row

private int mSeekLinePaddingX = 0; // Seek line padding x

private int mDisplayMode = DISPLAY_MODE_NORMAL;    private LrcViewListener mLrcViewListener;注:后续扩展在线歌词,歌词路径暂时写死在手机根目录,文件名为“林中鸟.lrc”。

扫描音乐

扫描音乐工具类

扫描音乐的实现逻辑如下,DISPLAY_NAME、TITLE、ARTIST、DATA、SIZE、DURATION分别对应了扫描音乐的外部可重命名的文件名、内部音乐文件名、歌手名称、文件所在路径、歌曲的大小、歌曲的时长信息。获取后可用于后续自定义扫描码、歌曲播放设置等处。isAudioControlPanelAvailable()方法是用来设置音效用的,后续会说到。object MusicUtils {    /**

* 扫描系统里面的音频文件,返回一个list集合

*/

fun getMusicData(context: Context): ArrayList {

val filter_size = PreferenceManager.getDefaultSharedPreferences(context).getString("filter_size", "0")

val filter_time = PreferenceManager.getDefaultSharedPreferences(context).getString("filter_time", "0")

val list = ArrayList()        // 媒体库查询语句(写一个工具类MusicUtils)

val cursor = context.contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.AudioColumns.IS_MUSIC)        if (cursor != null) {            while (cursor.moveToNext()) {

val music: Music? = Music()//                music!!.song = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME))

music!!.song = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE))

music.singer = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST))

music.path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA))

music.size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE))

music.duration = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION))                if (music.size.toInt()/1000 > filter_size.toInt() || music.duration!!.toInt()/1000 > filter_time.toInt()) {                    // 注释部分是切割标题,分离出歌曲名和歌手 (本地媒体库读取的歌曲信息不规范)

if (music.song!!.contains("-")) {

val str = music.song!!.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()

music.singer = str[0]

music.song = str[1]

}

list.add(music)

}

}            // 释放资源

cursor.close()

}        return list

}    fun isAudioControlPanelAvailable(context: Context): Boolean {        return isIntentAvailable(context, Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL))

}    private fun isIntentAvailable(context: Context, intent: Intent): Boolean {        return context.packageManager.resolveActivity(intent, PackageManager.GET_RESOLVED_FILTER) != null

}

}

扫描音乐动画

点击扫描音乐可实现雷达动画,音乐扫描完毕动画结束,实现扫描代码:fun scanMusic() {

val degrees = ObjectAnimator.ofInt(sv, "degrees", 0, 360)

degrees.interpolator = LinearInterpolator()

degrees.duration = 1000

degrees.repeatCount = ValueAnimator.INFINITE

degrees.start()

async({

MyApplication.instance!!.getMusicDao().deleteAll()            for (i in 0..getMusicData(MyApplication.instance!!).size - 1) {

val list = Music(i.toLong(), getMusicData(MyApplication.instance!!).get(i).song, getMusicData(MyApplication.instance!!).get(i).singer, getMusicData(MyApplication.instance!!).get(i).path)

MyApplication.instance!!.getMusicDao().insertInTx(list)

}

runOnUiThread {

degrees.cancel()

Toast.makeText(MyApplication.instance!!,"已扫描完毕",Toast.LENGTH_SHORT).show()

}

})

}

动画效果代码如下:public class ShaderView extends View {    /**

* 绘制扫描圈的笔

*/

private Paint mSweepPaint;    /**

* 绘制背景bitmap的笔

*/

private Paint mBitmapPaint;    /**

* 这个自定义View的宽度,就是你在xml布局里面设置的宽度(目前不支持)

*/

private int mWidth;    /**

* 背景图片

*/

private Bitmap mBitmap;    /**

* 雷达扫描旋转角度

*/

private int degrees = 0;    /**

* 用于控制扫描圈的矩阵

*/

Matrix mSweepMatrix = new Matrix();    /**

* 用于控制背景Bitmap的矩阵

*/

Matrix mBitmapMatrix = new Matrix();    /**

* 着色器---生成扫描圈

*/

private SweepGradient mSweepGradient;    /**

* 图片着色器

*/

private BitmapShader mBitmapShader;    private float mScale;    public ShaderView(Context context) {        super(context);

init();

}    public ShaderView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);

init();

}    /**

* 属性动画,必须有setXxx方法,才可以针对这个属性实现动画

*

* @param degrees

*/

public void setDegrees(int degrees) {        this.degrees = degrees;

postInvalidate();//在主线程里执行OnDraw

}    private void init() {//    1.准备好画笔

mSweepPaint = new Paint();

mBitmapPaint = new Paint();//    2.图片着色器

mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scan_music);

mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);//    3.将图片着色器设置给画笔

mBitmapPaint.setShader(mBitmapShader);

}    @Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);//    获取这个自定义view的宽高,注意在onMeasure里获取,在构造函数里得到的是0

mWidth = getMeasuredWidth();//    根据你所设置的view的尺寸和bitmap的尺寸计算一个缩放比例,否则的话,得到的图片是一个局部,而不是一整张图片

mScale = (float) mWidth / (float) mBitmap.getWidth();//    4.梯度扫描着色器

mSweepGradient = new SweepGradient(mWidth / 2, mWidth / 2, new int[]{Color.argb(50, 0, 0, 100), Color.argb(10, 30, 0, 0)}, null);//    5.将梯度扫描着色器设置给另外一支画笔

mSweepPaint.setShader(mSweepGradient);

}    @Override

protected void onDraw(Canvas canvas) {        super.onDraw(canvas);//    迫不得已的时候,才在onDraw方法写代码,能提前准备的要在之前去准备,//    不要写在onDraw里面,因为onDraw会不停地刷新绘制,写的代码越多,越影响效率//    将图片缩放至你指定的自定义View的宽高

mBitmapMatrix.setScale(mScale, mScale);

mBitmapShader.setLocalMatrix(mBitmapMatrix);//   设置扫描圈旋转角度

mSweepMatrix.setRotate(degrees, mWidth / 2, mWidth / 2);

mSweepGradient.setLocalMatrix(mSweepMatrix);//    5. 使用设置好图片着色器的画笔,画圆,先画出下层的背景图片,在画出上层的扫描图片

canvas.drawCircle(mWidth / 2, mWidth / 2, mWidth / 2, mBitmapPaint);

canvas.drawCircle(mWidth / 2, mWidth / 2, mWidth / 2, mSweepPaint);

}

}

实现音效设置

效果图:

AAffA0nNPuCLAAAAAElFTkSuQmCC

上文提到的isAudioControlPanelAvailable()方法在设置中的音效处调用。AudioSessionId获取当前播放的音乐sessionid,如果将AudioSessionId设为0,则所有在播放的音频都会被影响到。

调用代码:private fun startEqualizer() {            if (MusicUtils.isAudioControlPanelAvailable(activity)) {

val intent = Intent()

val packageName = activity.packageName

intent.action = AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL

intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)

intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)

intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, AudioSessionId)                try {

startActivityForResult(intent, 1)

} catch (e: ActivityNotFoundException) {

e.printStackTrace()

Toast.makeText(context, "设备不支持", Toast.LENGTH_SHORT).show()

}

} else {

Toast.makeText(context, "设备不支持", Toast.LENGTH_SHORT).show()

}

}

定时停止播放

音乐播放器一般会实现定时停止播放功能,向TimeStop()方法中输入需要定时的参数。

代码如下:fun TimeStop(time: Long) {        if (time.toInt() == 0 && job == null) {

Toast.makeText(this, "停止播放功能未开启", Toast.LENGTH_SHORT).show()

} else if (time.toInt() != 0) {

val time1 = time * 60000

Toast.makeText(this, "$time 分钟后停止播放", Toast.LENGTH_SHORT).show()

job = launch(CommonPool) {

delay(time1, TimeUnit.MILLISECONDS)

playService!!.pause()

}

} else {

job!!.cancel()

Toast.makeText(this, "已取消定时停止播放", Toast.LENGTH_SHORT).show()

}

}

歌曲控制

暂停播放等不在此赘述,具体可看源码,控制上一首下一首使用了Greendao数据库,扫码歌曲时,数据填入数据库中,下面是返回上一首的实现代码,下一首实现功能类似,使用Greendao主要是用来练练手,没有什么特别的原因。fun previous() {        if (position == 0) {

num = MyApplication.instance!!.getMusicDao().queryBuilder().list().size.plus(-1)

path = MyApplication.instance!!.getMusicDao().queryBuilder().where(MusicDao.Properties.Id.eq(num)).list()

} else {

num = position!!.plus(-1)

path = MyApplication.instance!!.getMusicDao().queryBuilder().where(MusicDao.Properties.Id.eq(num)).list()

}

prepare((path as MutableList?)!!.get(0).path)

start()

position = num

val intent = Intent("com.nilin.etherealmusic.play")

intent.putExtra("song", (path as MutableList?)!!.get(0).song)

intent.putExtra("singer", (path as MutableList?)!!.get(0).singer)

sendBroadcast(intent)

val editor = MyApplication.instance!!.getSharedPreferences("music_pref", Context.MODE_PRIVATE).edit()

editor.putString("song", (path as MutableList?)!!.get(0).song)

editor.putInt("position", position!!)

editor.apply()

}

启动引导页动画、搜索后音乐列表显示、音乐进度条拖拽快进、扫描音乐条件控制等不多赘述。功能实现并不复杂,为后续添加网络音乐功能做准备,部分功能暂未实现,后续会更新完善。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值