撸个应用学Android音乐App

前言

使用Kotlin实现一个简易的本地音乐客户端,部分代码为Java语言,Kotlin可以完美调用Java,Enjoy it!

三方库

  • Glide
  • Anko
  • BaseRecyclerViewAdapterHelper
  • Greendao

环境准备

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

效果图

183056l4c95644y2xoz3yc.gif

项目目录

183052rhv24e8kok99lutk.png

歌词显示

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

    

其中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);
    }
}

实现音效设置

上文提到的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()
    }

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

你可能感兴趣的:(撸个应用学Android音乐App)