Android项目实战 —— 手把手教你实现一款本地音乐播放器Dora Music

news2024/11/18 11:18:05

今天带大家实现一款基于Dora SDK的Android本地音乐播放器app,本项目也作为Dora SDK的实践项目或使用教程。使用到开源库有[https://github.com/dora4/dora] 、[https://github.com/dora4/dcache-android] 等。先声明一点,本项目主要作为框架的使用教程,界面风格不喜勿喷。

效果演示

1031691281506_.pic.jpg

1071691282562_.pic.jpg

1051691281511_.pic.jpg

1041691281509_.pic.jpg

实现功能

  1. 基本播放功能,包括播放、暂停、缓冲、后台播放等
  2. 播放模式切换
  3. 均衡器和重低音增强
  4. 耳机拔出暂停
  5. 音频焦点处理,和其他音乐播放器互斥
  6. 摇一摇切换歌曲
  7. 更换皮肤

知识产权

软著.jpeg

作品登记证书.png

框架搭建

我们要开发一款Android App,首先要搭建基础框架,比如使用MVP还是MVVM架构?使用什么网络库?使用什么ORM库?很显然,作为Dora SDK的使用教程,肯定是要依赖Dora SDK的。

 // Dora全家桶
    implementation("com.github.dora4:dcache-android:1.7.9")
    implementation("com.github.dora4:dora:1.1.9")
    implementation("com.github.dora4:dora-arouter-support:1.1")
    implementation("com.github.dora4:dora-apollo-support:1.1")
    implementation("com.github.dora4:dora-pgyer-support:1.0")
//    implementation 'com.github.dora4:dora-eventbus-support:1.1'
    implementation("com.github.dora4:dview-toggle-button:1.0")
    implementation("com.github.dora4:dview-alert-dialog:1.0")
    implementation("com.github.dora4:dview-loading-dialog:1.2")
    implementation("com.github.dora4:dview-colors:1.0")
    implementation("com.github.dora4:dview-skins:1.4")
    implementation("com.github.dora4:dview-bottom-dialog:1.1")
//    implementation 'com.github.dora4:dview-avatar:1.4'
    implementation("com.github.dora4:dview-titlebar:1.9")

列表功能使用BRVAH

implementation("io.github.cymchad:BaseRecyclerViewAdapterHelper:3.0.6")

运行时权限申请使用XXPermissions

implementation("com.github.getActivity:XXPermissions:18.2")

图片加载使用Glide

implementation("com.github.bumptech.glide:glide:4.11.0")

主要依赖的就是这些库。

应用入口MusicApp类编写

package site.doramusic.app

import dora.BaseApplication
import dora.db.Orm
import dora.db.OrmConfig
import dora.http.log.FormatLogInterceptor
import dora.http.retrofit.RetrofitManager
import site.doramusic.app.base.conf.AppConfig
import site.doramusic.app.db.Album
import site.doramusic.app.db.Artist
import site.doramusic.app.db.Folder
import site.doramusic.app.db.Music
import site.doramusic.app.http.service.CommonService
import site.doramusic.app.http.service.MusicService
import site.doramusic.app.http.service.UserService
import site.doramusic.app.media.MediaManager

class MusicApp : BaseApplication(), AppConfig {

    /**
     * 全局的音乐播放控制管理器。
     */
    var mediaManager: MediaManager? = null
        private set

    companion object {

        /**
         * 全局Application单例。
         */
        var instance: MusicApp? = null
            private set
    }

    override fun onCreate() {
        super.onCreate()
        instance = this
        init()
    }

    private fun init() {
        initHttp()   // 初始化网络框架
        initDb()    // 初始化SQLite数据库的表
        initMedia() // 初始化媒体管理器
    }

    private fun initMedia() {
        mediaManager = MediaManager(this)
    }

    private fun initHttp() {
        RetrofitManager.initConfig {
            okhttp {
                interceptors().add(FormatLogInterceptor())
                build()
            }
            mappingBaseUrl(MusicService::class.java, AppConfig.URL_APP_SERVER)
            mappingBaseUrl(UserService::class.java, AppConfig.URL_APP_SERVER)
            mappingBaseUrl(CommonService::class.java, AppConfig.URL_CHAT_SERVER)
        }
    }

    private fun initDb() {
        Orm.init(this, OrmConfig.Builder()
            .database(AppConfig.DB_NAME)
            .version(AppConfig.DB_VERSION)
            .tables(Music::class.java, Artist::class.java,
                Album::class.java, Folder::class.java)
            .build())
    }
}

网络和ORM库都是来自于dcache-android库。首先初始化4张表,music、artist、album、folder,用来保存一些音乐信息。初始化网络库的时候添加一个FormatLogInterceptor日志拦截器,方便格式化输出网络请求日志。在Application中保存一个MediaManager单例,用来全局控制音乐的播放、暂停等。

MediaManager与整体媒体框架

我们使用MediaManager来统一管理媒体。由于要支持app后台运行时也能继续播放,所以我们考虑使用Service,而我们这不是一个简简单单的服务,而是要实时控制和反馈数据的。对于这样的一种场景,我们考虑将服务运行在单独的进程,并使用AIDL在主进程进行跨进程调用。


/**
 * 通过它调用AIDL远程服务接口。
 */
class MediaManager(internal val context: Context) : IMediaService.Stub(), AppConfig {

    private var mediaService: IMediaService? = null
    private val serviceConnection: ServiceConnection
    private var onCompletionListener: MusicControl.OnConnectCompletionListener? = null

    init {
        this.serviceConnection = object : ServiceConnection {

            override fun onServiceConnected(name: ComponentName, service: IBinder) {
                mediaService = asInterface(service)
                if (mediaService != null) {
                    //音频服务启动的标志
                    LogUtils.i("MediaManager:connected")
                    onCompletionListener!!.onConnectCompletion(mediaService)
                }
            }

            override fun onServiceDisconnected(name: ComponentName) {
                //音频服务断开的标志
                LogUtils.i("MediaManager:disconnected")
            }
        }
    }

    fun setOnCompletionListener(l: MusicControl.OnConnectCompletionListener) {
        onCompletionListener = l
    }

    fun connectService() {
        val intent = Intent(AppConfig.MEDIA_SERVICE)
        intent.setClass(context, MediaService::class.java)
        context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
    }

    fun disconnectService() {
        context.unbindService(serviceConnection)
        context.stopService(Intent(AppConfig.MEDIA_SERVICE))
    }

    override fun play(pos: Int): Boolean {
        try {
            return mediaService?.play(pos) ?: false
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return false
    }

    override fun playById(id: Int): Boolean {
        try {
            return mediaService?.playById(id) ?: false
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return false
    }

    override fun playByPath(path: String) {
        try {
            mediaService?.playByPath(path)
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun playByUrl(music: Music, url: String) {
        try {
            mediaService?.playByUrl(music, url)
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun replay(): Boolean {
        try {
            return mediaService?.replay() ?: false
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return false
    }

    override fun pause(): Boolean {
        try {
            return mediaService?.pause() ?: false
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return false
    }

    override fun prev(): Boolean {
        try {
            return mediaService?.prev() ?: false
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return false
    }

    override fun next(): Boolean {
        try {
            return mediaService?.next() ?: false
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return false
    }

    override fun stop() {
        try {
            mediaService?.stop() ?: false
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun duration(): Int {
        try {
            return mediaService?.duration() ?: 0
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return 0
    }

    override fun setCurMusic(music: Music) {
        try {
            mediaService?.setCurMusic(music) ?: false
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun position(): Int {
        try {
            return mediaService?.position() ?: 0
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return 0
    }

    override fun pendingProgress(): Int {
        try {
            return mediaService?.pendingProgress() ?: 0
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return 0
    }

    override fun seekTo(progress: Int): Boolean {
        try {
            return mediaService?.seekTo(progress) ?: false
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return false
    }

    override fun refreshPlaylist(playlist: MutableList<Music>?) {
        try {
            mediaService?.refreshPlaylist(playlist)
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun setBassBoost(strength: Int) {
        try {
            mediaService?.setBassBoost(strength)
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun setEqualizer(bandLevels: IntArray) {
        try {
            mediaService?.setEqualizer(bandLevels)
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun getEqualizerFreq(): IntArray? {
        try {
            return mediaService?.equalizerFreq
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return null
    }

    override fun getPlayState(): Int {
        try {
            return mediaService?.playState ?: 0
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return 0
    }

    override fun getPlayMode(): Int {
        try {
            return mediaService?.playMode ?: 0
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return 0
    }

    override fun setPlayMode(mode: Int) {
        try {
            mediaService?.playMode = mode
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun getCurMusicId(): Int {
        try {
            return mediaService?.curMusicId ?: -1
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return -1
    }

    override fun loadCurMusic(music: Music): Boolean {
        try {
            return mediaService?.loadCurMusic(music) ?: false
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return false
    }

    override fun getCurMusic(): Music? {
        try {
            return mediaService?.curMusic
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
        return null
    }

    override fun getPlaylist(): MutableList<Music>? {
        try {
            return mediaService?.playlist
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return null
    }

    override fun updateNotification(bitmap: Bitmap, title: String, name: String) {
        try {
            mediaService?.updateNotification(bitmap, title, name)
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun cancelNotification() {
        try {
            mediaService?.cancelNotification()
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }
}

我们将服务配置在单独的进程,需要在AndroidManifest.xml中给service标签指定android:process,也就是进程标识,这样就分出了区别于应用主进程的一个新的进程。

<service
    android:name=".media.MediaService"
    android:process=":doramedia"
    android:exported="true"
    android:label="DoraMusic Media">
    <intent-filter>
        <action android:name="site.doramusic.app.service.ACTION_MEDIA_SERVICE" />
    </intent-filter>
</service>

与媒体信息相关表的定义

Music歌曲表

package site.doramusic.app.db;

import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;

import androidx.annotation.NonNull;

import dora.db.constraint.AssignType;
import dora.db.constraint.PrimaryKey;
import dora.db.migration.OrmMigration;
import dora.db.table.Column;
import dora.db.table.Ignore;
import dora.db.table.OrmTable;
import dora.db.table.PrimaryKeyEntry;
import dora.db.table.Table;
import site.doramusic.app.sort.Sort;

/**
 * 歌曲表。
 */
@Table("music")
public class Music implements OrmTable, Parcelable, Sort {

    public static final String COLUMN_ID = "_id";
    public static final String COLUMN_SONG_ID = "song_id";
    public static final String COLUMN_ALBUM_ID = "album_id";
    public static final String COLUMN_DURATION = "duration";
    public static final String COLUMN_MUSIC_NAME = "music_name";
    public static final String COLUMN_ARTIST = "artist";
    public static final String COLUMN_DATA = "data";
    public static final String COLUMN_FOLDER = "folder";
    public static final String COLUMN_MUSIC_NAME_KEY = "music_name_key";
    public static final String COLUMN_ARTIST_KEY = "artist_key";
    public static final String COLUMN_FAVORITE = "favorite";
    public static final String COLUMN_LAST_PLAY_TIME = "last_play_time";

    /**
     * 数据库中的_id
     */
    @Column(COLUMN_ID)
    @PrimaryKey(AssignType.AUTO_INCREMENT)
    public int id;
    @Column(COLUMN_SONG_ID)
    public int songId = -1;
    @Column(COLUMN_ALBUM_ID)
    public int albumId = -1;
    @Column(COLUMN_DURATION)
    public int duration;
    @Column(COLUMN_MUSIC_NAME)
    public String musicName;
    @Column(COLUMN_ARTIST)
    public String artist;
    @Column(COLUMN_DATA)
    public String data;
    @Column(COLUMN_FOLDER)
    public String folder;
    @Column(COLUMN_MUSIC_NAME_KEY)
    public String musicNameKey;
    @Column(COLUMN_ARTIST_KEY)
    public String artistKey;
    @Column(COLUMN_FAVORITE)
    public int favorite;
    @Column(COLUMN_LAST_PLAY_TIME)
    public long lastPlayTime;
    @Ignore
    private String sortLetter;
    @Ignore
    private Type type;

    /**
     * 封面路径,在线歌曲用。
     */
    @Ignore
    private String coverPath;

    @NonNull
    @Override
    public OrmMigration[] getMigrations() {
        return new OrmMigration[0];
    }

    public enum  Type {
        LOCAL, ONLINE
    }

    public Music() {
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        Bundle bundle = new Bundle();
        bundle.putInt(COLUMN_ID, id);
        bundle.putInt(COLUMN_SONG_ID, songId);
        bundle.putInt(COLUMN_ALBUM_ID, albumId);
        bundle.putInt(COLUMN_DURATION, duration);
        bundle.putString(COLUMN_MUSIC_NAME, musicName);
        bundle.putString(COLUMN_ARTIST, artist);
        bundle.putString(COLUMN_DATA, data);
        bundle.putString(COLUMN_FOLDER, folder);
        bundle.putString(COLUMN_MUSIC_NAME_KEY, musicNameKey);
        bundle.putString(COLUMN_ARTIST_KEY, artistKey);
        bundle.putInt(COLUMN_FAVORITE, favorite);
        bundle.putLong(COLUMN_LAST_PLAY_TIME, lastPlayTime);
        dest.writeBundle(bundle);
    }

    public static final Creator<Music> CREATOR = new Creator<Music>() {

        @Override
        public Music createFromParcel(Parcel source) {
            Music music = new Music();
            Bundle bundle = source.readBundle(getClass().getClassLoader());
            music.id = bundle.getInt(COLUMN_ID);
            music.songId = bundle.getInt(COLUMN_SONG_ID);
            music.albumId = bundle.getInt(COLUMN_ALBUM_ID);
            music.duration = bundle.getInt(COLUMN_DURATION);
            music.musicName = bundle.getString(COLUMN_MUSIC_NAME);
            music.artist = bundle.getString(COLUMN_ARTIST);
            music.data = bundle.getString(COLUMN_DATA);
            music.folder = bundle.getString(COLUMN_FOLDER);
            music.musicNameKey = bundle.getString(COLUMN_MUSIC_NAME_KEY);
            music.artistKey = bundle.getString(COLUMN_ARTIST_KEY);
            music.favorite = bundle.getInt(COLUMN_FAVORITE);
            music.lastPlayTime = bundle.getLong(COLUMN_LAST_PLAY_TIME);
            return music;
        }

        @Override
        public Music[] newArray(int size) {
            return new Music[size];
        }
    };

    @NonNull
    @Override
    public String toString() {
        return "DoraMusic{" +
                "id=" + id +
                ", songId=" + songId +
                ", albumId=" + albumId +
                ", duration=" + duration +
                ", musicName='" + musicName + ''' +
                ", artist='" + artist + ''' +
                ", data='" + data + ''' +
                ", folder='" + folder + ''' +
                ", musicNameKey='" + musicNameKey + ''' +
                ", artistKey='" + artistKey + ''' +
                ", favorite=" + favorite +
                ", lastPlayTime=" + lastPlayTime +
                '}';
    }

    @NonNull
    @Override
    public PrimaryKeyEntry getPrimaryKey() {
        return new PrimaryKeyEntry(COLUMN_ID, id);
    }

    @Override
    public boolean isUpgradeRecreated() {
        return false;
    }

    @Override
    public String getSortLetter() {
        return sortLetter;
    }

    @Override
    public void setSortLetter(String sortLetter) {
        this.sortLetter = sortLetter;
    }

    public void setType(Type type) {
        this.type = type;
    }

    public Type getType() {
        return type;
    }

    public void setCoverPath(String coverPath) {
        this.coverPath = coverPath;
    }

    public String getCoverPath() {
        return coverPath;
    }

    @Override
    public int compareTo(Sort sort) {
        return getSortLetter().compareTo(sort.getSortLetter());
    }
}

Artist歌手表

package site.doramusic.app.db;

import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;

import androidx.annotation.NonNull;

import dora.db.constraint.AssignType;
import dora.db.constraint.PrimaryKey;
import dora.db.migration.OrmMigration;
import dora.db.table.Column;
import dora.db.table.Ignore;
import dora.db.table.OrmTable;
import dora.db.table.PrimaryKeyEntry;
import dora.db.table.Table;
import site.doramusic.app.sort.Sort;

/**
 * 歌手表。
 */
@Table("artist")
public class Artist implements OrmTable, Parcelable, Sort {

    public static final String COLUMN_ID = "_id";
    public static final String COLUMN_ARTIST_NAME = "artist_name";
    public static final String COLUMN_NUMBER_OF_TRACKS = "number_of_tracks";

    @Ignore
    private String sortLetter;

    @Column(COLUMN_ID)
    @PrimaryKey(AssignType.AUTO_INCREMENT)
    public int id;

    @Column(COLUMN_ARTIST_NAME)
    public String name;

    /**
     * 曲目数。
     */
    @Column(COLUMN_NUMBER_OF_TRACKS)
    public int number_of_tracks;

    @Override
    public int describeContents() {
        return 0;
    }


    public Artist() {
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        Bundle bundle = new Bundle();
        bundle.putInt(COLUMN_ID, id);
        bundle.putString(COLUMN_ARTIST_NAME, name);
        bundle.putInt(COLUMN_NUMBER_OF_TRACKS, number_of_tracks);
        dest.writeBundle(bundle);
    }

    public static final Creator<Artist> CREATOR = new Creator<Artist>() {

        @Override
        public Artist createFromParcel(Parcel source) {
            Bundle bundle = source.readBundle(getClass().getClassLoader());
            Artist artist = new Artist();
            artist.id = bundle.getInt(COLUMN_ID);
            artist.name = bundle.getString(COLUMN_ARTIST_NAME);
            artist.number_of_tracks = bundle.getInt(COLUMN_NUMBER_OF_TRACKS);
            return artist;
        }

        @Override
        public Artist[] newArray(int size) {
            return new Artist[size];
        }
    };

    @NonNull
    @Override
    public PrimaryKeyEntry getPrimaryKey() {
        return new PrimaryKeyEntry(COLUMN_ID, id);
    }

    @Override
    public boolean isUpgradeRecreated() {
        return false;
    }

    @Override
    public String getSortLetter() {
        return sortLetter;
    }

    @Override
    public void setSortLetter(String sortLetter) {
        this.sortLetter = sortLetter;
    }

    public int compareTo(Sort sort) {
        return getSortLetter().compareTo(sort.getSortLetter());
    }

    @NonNull
    @Override
    public OrmMigration[] getMigrations() {
        return new OrmMigration[0];
    }
}

Album专辑表

package site.doramusic.app.db;

import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;

import androidx.annotation.NonNull;

import dora.db.constraint.AssignType;
import dora.db.constraint.PrimaryKey;
import dora.db.migration.OrmMigration;
import dora.db.table.Column;
import dora.db.table.Ignore;
import dora.db.table.OrmTable;
import dora.db.table.PrimaryKeyEntry;
import dora.db.table.Table;
import site.doramusic.app.sort.Sort;

/**
 * 专辑表。
 */
@Table("album")
public class Album implements OrmTable, Parcelable, Sort {

    public static final String COLUMN_ID = "_id";
    public static final String COLUMN_ALBUM_NAME = "album_name";
    public static final String COLUMN_ALBUM_ID = "album_id";
    public static final String COLUMN_NUMBER_OF_SONGS = "number_of_songs";
    public static final String COLUMN_ALBUM_COVER_PATH = "album_cover_path";

    @Column(COLUMN_ID)
    @PrimaryKey(AssignType.AUTO_INCREMENT)
    public int id;

    @Ignore
    private String sortLetter;

    //专辑名称
    @Column(COLUMN_ALBUM_NAME)
    public String album_name;
    //专辑在数据库中的id
    @Column(COLUMN_ALBUM_ID)
    public int album_id = -1;
    //专辑的歌曲数目
    @Column(COLUMN_NUMBER_OF_SONGS)
    public int number_of_songs = 0;
    //专辑封面图片路径
    @Column(COLUMN_ALBUM_COVER_PATH)
    public String album_cover_path;

    @Override
    public int describeContents() {
        return 0;
    }


    public Album() {
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        Bundle bundle = new Bundle();
        bundle.putInt(COLUMN_ID, id);
        bundle.putString(COLUMN_ALBUM_NAME, album_name);
        bundle.putString(COLUMN_ALBUM_COVER_PATH, album_cover_path);
        bundle.putInt(COLUMN_NUMBER_OF_SONGS, number_of_songs);
        bundle.putInt(COLUMN_ALBUM_ID, album_id);
        dest.writeBundle(bundle);
    }

    public static final Creator<Album> CREATOR = new Creator<Album>() {

        @Override
        public Album createFromParcel(Parcel source) {
            Album album = new Album();
            Bundle bundle = source.readBundle(getClass().getClassLoader());
            album.id = bundle.getInt(COLUMN_ID);
            album.album_name = bundle.getString(COLUMN_ALBUM_NAME);
            album.album_cover_path = bundle.getString(COLUMN_ALBUM_COVER_PATH);
            album.number_of_songs = bundle.getInt(COLUMN_NUMBER_OF_SONGS);
            album.album_id = bundle.getInt(COLUMN_ALBUM_ID);
            return album;
        }

        @Override
        public Album[] newArray(int size) {
            return new Album[size];
        }
    };

    @NonNull
    @Override
    public PrimaryKeyEntry getPrimaryKey() {
        return new PrimaryKeyEntry(COLUMN_ID, id);
    }

    @Override
    public boolean isUpgradeRecreated() {
        return false;
    }

    @Override
    public String getSortLetter() {
        return sortLetter;
    }

    @Override
    public void setSortLetter(String sortLetter) {
        this.sortLetter = sortLetter;
    }

    @Override
    public int compareTo(Sort sort) {
        return getSortLetter().compareTo(sort.getSortLetter());
    }

    @NonNull
    @Override
    public OrmMigration[] getMigrations() {
        return new OrmMigration[0];
    }
}

Folder文件夹表

package site.doramusic.app.db;

import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;

import androidx.annotation.NonNull;

import dora.db.constraint.AssignType;
import dora.db.constraint.NotNull;
import dora.db.constraint.PrimaryKey;
import dora.db.constraint.Unique;
import dora.db.migration.OrmMigration;
import dora.db.table.Column;
import dora.db.table.Ignore;
import dora.db.table.OrmTable;
import dora.db.table.PrimaryKeyEntry;
import dora.db.table.Table;
import site.doramusic.app.sort.Sort;

/**
 * 文件夹表。
 */
@Table("folder")
public class Folder implements OrmTable, Parcelable, Sort {

    public static final String COLUMN_ID = "_id";
    public static final String COLUMN_FOLDER_NAME = "folder_name";
    public static final String COLUMN_FOLDER_PATH = "folder_path";

    @Ignore
    private String sortLetter;

    @Column(COLUMN_ID)
    @PrimaryKey(AssignType.AUTO_INCREMENT)
    public int id;
    @Column(COLUMN_FOLDER_NAME)
    public String name;
    @Unique
    @NotNull
    @Column(COLUMN_FOLDER_PATH)
    public String path;

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        Bundle bundle = new Bundle();
        bundle.putInt(COLUMN_ID, id);
        bundle.putString(COLUMN_FOLDER_NAME, name);
        bundle.putString(COLUMN_FOLDER_PATH, path);
        dest.writeBundle(bundle);
    }

    public Folder() {
    }

    public static Creator<Folder> CREATOR = new Creator<Folder>() {

        @Override
        public Folder createFromParcel(Parcel source) {
            Folder folder = new Folder();
            Bundle bundle = source.readBundle(getClass().getClassLoader());
            folder.id = bundle.getInt(COLUMN_ID);
            folder.name = bundle.getString(COLUMN_FOLDER_NAME);
            folder.path = bundle.getString(COLUMN_FOLDER_PATH);
            return folder;
        }

        @Override
        public Folder[] newArray(int size) {
            return new Folder[size];
        }
    };

    @NonNull
    @Override
    public PrimaryKeyEntry getPrimaryKey() {
        return new PrimaryKeyEntry(COLUMN_ID, id);
    }

    @Override
    public boolean isUpgradeRecreated() {
        return false;
    }

    @Override
    public String getSortLetter() {
        return sortLetter;
    }

    @Override
    public void setSortLetter(String sortLetter) {
        this.sortLetter = sortLetter;
    }

    @Override
    public int compareTo(Sort sort) {
        return getSortLetter().compareTo(sort.getSortLetter());
    }

    @NonNull
    @Override
    public OrmMigration[] getMigrations() {
        return new OrmMigration[0];
    }
}

这4张表的类主要演示dcache-android库的orm功能。我们可以看到@Table和@Column可以给表和列重命名,当然,不一定就会使用默认的表和列名规则。不是表字段的属性加上@Ignore。也可以通过@Unique配置唯一约束,通过@NotNull配置非空约束,使用@PrimaryKey配置主键约束。

MusicScanner本地歌曲扫描

package site.doramusic.app.media

import android.annotation.SuppressLint
import android.content.Context
import android.database.Cursor
import android.provider.MediaStore
import dora.db.Orm
import dora.db.Transaction
import dora.db.dao.DaoFactory
import dora.db.table.TableManager
import dora.util.PinyinUtils
import dora.util.TextUtils
import site.doramusic.app.base.conf.AppConfig
import site.doramusic.app.db.Album
import site.doramusic.app.db.Artist
import site.doramusic.app.db.Folder
import site.doramusic.app.db.Music
import site.doramusic.app.util.MusicUtils
import site.doramusic.app.util.PreferencesManager
import java.io.File
import java.util.*
import kotlin.collections.ArrayList

/**
 * 媒体扫描器,用来扫描手机中的歌曲文件。
 */
@SuppressLint("Range")
object MusicScanner : AppConfig {

    private val proj_music = arrayOf(
            MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE,
            MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID,
            MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ARTIST_ID,
            MediaStore.Audio.Media.DURATION)
    private val proj_album = arrayOf(MediaStore.Audio.Albums.ALBUM,
            MediaStore.Audio.Albums.NUMBER_OF_SONGS, MediaStore.Audio.Albums._ID,
            MediaStore.Audio.Albums.ALBUM_ART)
    private val proj_artist = arrayOf(
            MediaStore.Audio.Artists.ARTIST,
            MediaStore.Audio.Artists.NUMBER_OF_TRACKS)
    private val proj_folder = arrayOf(MediaStore.Files.FileColumns.DATA)

    private val musicDao = DaoFactory.getDao(Music::class.java)
    private val artistDao = DaoFactory.getDao(Artist::class.java)
    private val albumDao = DaoFactory.getDao(Album::class.java)
    private val folderDao = DaoFactory.getDao(Folder::class.java)

    private fun recreateTables() {
        TableManager.recreateTable(Music::class.java)
        TableManager.recreateTable(Artist::class.java)
        TableManager.recreateTable(Album::class.java)
        TableManager.recreateTable(Folder::class.java)
    }

    @JvmStatic
    fun scan(context: Context): List<Music> {
        recreateTables()
        var musics = arrayListOf<Music>()
        Transaction.execute(Music::class.java) {
            musics = queryMusic(context, AppConfig.ROUTE_START_FROM_LOCAL) as ArrayList<Music>
            it.insert(musics)
        }
        if (musics.size > 0) {
            // 歌曲都没有就没有必要查询歌曲信息了
            Transaction.execute {
                val artists = queryArtist(context)
                artistDao.insert(artists)
                val albums = queryAlbum(context)
                albumDao.insert(albums)
                val folders = queryFolder(context)
                folderDao.insert(folders)
            }
        }
        return musics
    }

    @JvmStatic
    fun queryMusic(context: Context, from: Int): List<Music> {
        return queryMusic(context, null, null, from)
    }

    @JvmStatic
    fun queryMusic(context: Context,
                   selections: String?, selection: String?, from: Int): List<Music> {
        val sp = PreferencesManager(context)
        val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
        val cr = context.contentResolver
        val select = StringBuffer(" 1=1 ")
        // 查询语句:检索出.mp3为后缀名,时长大于1分钟,文件大小大于1MB的媒体文件
        if (sp.getFilterSize()) {
            select.append(" and ${MediaStore.Audio.Media.SIZE} > " +
                    "${AppConfig.SCANNER_FILTER_SIZE}")
        }
        if (sp.getFilterTime()) {
            select.append(" and ${MediaStore.Audio.Media.DURATION} > " +
                    "${AppConfig.SCANNER_FILTER_DURATION}")
        }
        if (TextUtils.isNotEmpty(selections)) {
            select.append(selections)
        }
        return when (from) {
            AppConfig.ROUTE_START_FROM_LOCAL -> if (musicDao.count() > 0) {
                musicDao.selectAll()
            } else {
                getMusicList(cr.query(uri, proj_music,
                        select.toString(), null,
                        MediaStore.Audio.Media.ARTIST_KEY))
            }
            AppConfig.ROUTE_START_FROM_ARTIST -> if (musicDao.count() > 0) {
                queryMusic(selection,
                        AppConfig.ROUTE_START_FROM_ARTIST)
            } else {
                getMusicList(cr.query(uri, proj_music,
                        select.toString(), null,
                        MediaStore.Audio.Media.ARTIST_KEY))
            }
            AppConfig.ROUTE_START_FROM_ALBUM -> {
                if (musicDao.count() > 0) {
                    return queryMusic(selection,
                            AppConfig.ROUTE_START_FROM_ALBUM)
                }
                if (musicDao.count() > 0) {
                    return queryMusic(selection, AppConfig.ROUTE_START_FROM_FOLDER)
                }
                if (musicDao.count() > 0) {
                    return queryMusic(selection, AppConfig.ROUTE_START_FROM_FAVORITE)
                }
                if (musicDao.count() > 0) {
                    queryMusic(selection, AppConfig.ROUTE_START_FROM_LATEST)
                } else arrayListOf()
            }
            AppConfig.ROUTE_START_FROM_FOLDER -> {
                if (musicDao.count() > 0) {
                    return queryMusic(selection, AppConfig.ROUTE_START_FROM_FOLDER)
                }
                if (musicDao.count() > 0) {
                    return queryMusic(selection, AppConfig.ROUTE_START_FROM_FAVORITE)
                }
                if (musicDao.count() > 0) {
                    queryMusic(selection, AppConfig.ROUTE_START_FROM_LATEST)
                } else arrayListOf()
            }
            AppConfig.ROUTE_START_FROM_FAVORITE -> {
                if (musicDao.count() > 0) {
                    return queryMusic(selection, AppConfig.ROUTE_START_FROM_FAVORITE)
                }
                if (musicDao.count() > 0) {
                    queryMusic(selection, AppConfig.ROUTE_START_FROM_LATEST)
                } else arrayListOf()
            }
            AppConfig.ROUTE_START_FROM_LATEST -> {
                if (musicDao.count() > 0) {
                    queryMusic(selection, AppConfig.ROUTE_START_FROM_LATEST)
                } else arrayListOf()
            }
            else -> arrayListOf()
        }
    }

    @JvmStatic
    fun queryMusic(selection: String?, type: Int): List<Music> {
        val db = Orm.getDB()
        var sql = ""
        when (type) {
            AppConfig.ROUTE_START_FROM_ARTIST -> {
                sql = "select * from music where ${Music.COLUMN_ARTIST} = ?"
            }
            AppConfig.ROUTE_START_FROM_ALBUM -> {
                sql = "select * from music where ${Music.COLUMN_ALBUM_ID} = ?"
            }
            AppConfig.ROUTE_START_FROM_FOLDER -> {
                sql = "select * from music where ${Music.COLUMN_FOLDER} = ?"
            }
            AppConfig.ROUTE_START_FROM_FAVORITE -> {
                sql = "select * from music where ${Music.COLUMN_FAVORITE} = ?"
                //        } else if (type == ROUTE_START_FROM_DOWNLOAD) {
//            sql = "select * from music where download = ?";
            }
            AppConfig.ROUTE_START_FROM_LATEST -> {
                sql = "select * from music where ${Music.COLUMN_LAST_PLAY_TIME} > ? order by " +
                        "${Music.COLUMN_LAST_PLAY_TIME} desc limit 100"
            }
        }
        return parseCursor(db.rawQuery(sql, arrayOf(selection)))
    }

    private fun parseCursor(cursor: Cursor): List<Music> {
        val list: MutableList<Music> = ArrayList()
        while (cursor.moveToNext()) {
            val music = Music()
            music.id = cursor.getInt(cursor.getColumnIndex(Music.COLUMN_ID))
            music.songId = cursor.getInt(cursor.getColumnIndex(Music.COLUMN_SONG_ID))
            music.albumId = cursor.getInt(cursor.getColumnIndex(Music.COLUMN_ALBUM_ID))
            music.duration = cursor.getInt(cursor.getColumnIndex(Music.COLUMN_DURATION))
            music.musicName = cursor.getString(cursor.getColumnIndex(
                Music.COLUMN_MUSIC_NAME))
            music.artist = cursor.getString(cursor.getColumnIndex(Music.COLUMN_ARTIST))
            music.data = cursor.getString(cursor.getColumnIndex(Music.COLUMN_DATA))
            music.folder = cursor.getString(cursor.getColumnIndex(Music.COLUMN_FOLDER))
            music.musicNameKey = cursor.getString(cursor.getColumnIndex(
                Music.COLUMN_MUSIC_NAME_KEY))
            music.artistKey = cursor.getString(cursor.getColumnIndex(
                Music.COLUMN_ARTIST_KEY))
            music.favorite = cursor.getInt(cursor.getColumnIndex(Music.COLUMN_FAVORITE))
            music.lastPlayTime = cursor.getLong(cursor.getColumnIndex(
                Music.COLUMN_LAST_PLAY_TIME))
            list.add(music)
        }
        cursor.close()
        return list
    }

    /**
     * 获取包含音频文件的文件夹信息。
     *
     * @param context
     * @return
     */
    @JvmStatic
    fun queryFolder(context: Context): List<Folder> {
        val sp = PreferencesManager(context)
        val uri = MediaStore.Files.getContentUri("external")
        val cr = context.contentResolver
        val selection = StringBuilder(MediaStore.Files.FileColumns.MEDIA_TYPE
                + " = " + MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO + " and " + "("
                + MediaStore.Files.FileColumns.DATA + " like '%.mp3' or "
                + MediaStore.Files.FileColumns.DATA + " like '%.flac' or "
                + MediaStore.Files.FileColumns.DATA + " like '%.wav' or "
                + MediaStore.Files.FileColumns.DATA + " like '%.ape' or "
                + MediaStore.Files.FileColumns.DATA + " like '%.m4a' or "
                + MediaStore.Files.FileColumns.DATA + " like '%.aac')")
        // 查询语句:检索出.mp3为后缀名,时长大于1分钟,文件大小大于1MB的媒体文件
        if (sp.getFilterSize()) {
            selection.append(" and " + MediaStore.Audio.Media.SIZE + " > " + AppConfig.SCANNER_FILTER_SIZE)
        }
        if (sp.getFilterTime()) {
            selection.append(" and " + MediaStore.Audio.Media.DURATION + " > " + AppConfig.SCANNER_FILTER_DURATION)
        }
//        selection.append(") group by ( " + MediaStore.Files.FileColumns.PARENT)
        return if (folderDao.count() > 0) {
            folderDao.selectAll()
        } else {
            getFolderList(cr.query(uri, proj_folder, selection.toString(), null, null))
        }
    }

    /**
     * 获取歌手信息。
     *
     * @param context
     * @return
     */
    @JvmStatic
    fun queryArtist(context: Context): List<Artist> {
        val uri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI
        val cr = context.contentResolver
        return if (artistDao.count() > 0) {
            artistDao.selectAll()
        } else {
            getArtistList(cr.query(uri, proj_artist,
                    null, null, MediaStore.Audio.Artists.NUMBER_OF_TRACKS
                    + " desc"))
        }
    }

    /**
     * 获取专辑信息。
     *
     * @param context
     * @return
     */
    @JvmStatic
    fun queryAlbum(context: Context): List<Album> {
        val sp = PreferencesManager(context)
        val uri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI
        val cr = context.contentResolver
        val where = StringBuilder(MediaStore.Audio.Albums._ID
                + " in (select distinct " + MediaStore.Audio.Media.ALBUM_ID
                + " from audio_meta where (1=1 ")
        if (sp.getFilterSize()) {
            where.append(" and " + MediaStore.Audio.Media.SIZE + " > " + AppConfig.SCANNER_FILTER_SIZE)
        }
        if (sp.getFilterTime()) {
            where.append(" and " + MediaStore.Audio.Media.DURATION + " > " + AppConfig.SCANNER_FILTER_DURATION)
        }
        where.append("))")
        return if (albumDao.count() > 0) {
            albumDao.selectAll()
        } else { // Media.ALBUM_KEY 按专辑名称排序
            // FIXME:  Android11的Invalid token select问题
            getAlbumList(cr.query(uri, proj_album,
                    null, null, MediaStore.Audio.Media.ALBUM_KEY))
        }
    }

    private fun getMusicList(cursor: Cursor?): List<Music> {
        val list: MutableList<Music> = ArrayList()
        if (cursor == null) {
            return list
        }
        while (cursor.moveToNext()) {
            val music = Music()
            val filePath = cursor.getString(cursor
                    .getColumnIndex(MediaStore.Audio.Media.DATA))
            music.songId = cursor.getInt(cursor
                    .getColumnIndex(MediaStore.Audio.Media._ID))
            music.albumId = cursor.getInt(cursor
                    .getColumnIndex(MediaStore.Audio.Media.ALBUM_ID))
            val duration = cursor.getInt(cursor
                    .getColumnIndex(MediaStore.Audio.Media.DURATION))
            if (duration > 0) {
                music.duration = duration
            } else {
                try {
                    music.duration = MusicUtils.getDuration(filePath)
                } catch (e: RuntimeException) {
                    continue
                }
            }
            music.musicName = cursor.getString(cursor
                    .getColumnIndex(MediaStore.Audio.Media.TITLE))
            music.artist = cursor.getString(cursor
                    .getColumnIndex(MediaStore.Audio.Media.ARTIST))
            music.data = filePath
            val folderPath = filePath.substring(0,
                    filePath.lastIndexOf(File.separator))
            music.folder = folderPath
            music.musicNameKey = PinyinUtils.getPinyinFromSentence(music.musicName)
            music.artistKey = PinyinUtils.getPinyinFromSentence(music.artist)
            list.add(music)
        }
        cursor.close()
        return list
    }

    private fun getAlbumList(cursor: Cursor?): List<Album> {
        val list: MutableList<Album> = ArrayList()
        if (cursor == null) {
            return list
        }
        while (cursor.moveToNext()) {
            val album = Album()
            album.album_name = cursor.getString(
                cursor.getColumnIndex(MediaStore.Audio.Albums.ALBUM))
            album.album_id = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Albums._ID))
            album.number_of_songs = cursor.getInt(cursor
                    .getColumnIndex(MediaStore.Audio.Albums.NUMBER_OF_SONGS))
            album.album_cover_path = cursor.getString(cursor
                    .getColumnIndex(MediaStore.Audio.Albums.ALBUM_ART))
            list.add(album)
        }
        cursor.close()
        return list
    }

    private fun getArtistList(cursor: Cursor?): List<Artist> {
        val list: MutableList<Artist> = ArrayList()
        if (cursor == null) {
            return list
        }
        while (cursor.moveToNext()) {
            val artist = Artist()
            artist.name = cursor.getString(cursor
                    .getColumnIndex(MediaStore.Audio.Artists.ARTIST))
            artist.number_of_tracks = cursor.getInt(cursor
                    .getColumnIndex(MediaStore.Audio.Artists.NUMBER_OF_TRACKS))
            list.add(artist)
        }
        cursor.close()
        return list
    }

    private fun getFolderList(cursor: Cursor?): List<Folder> {
        val list: MutableList<Folder> = ArrayList()
        if (cursor == null) {
            return list
        }
        while (cursor.moveToNext()) {
            val folder = Folder()
            val filePath = cursor.getString(
                cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA))
            folder.path = filePath.substring(0,
                    filePath.lastIndexOf(File.separator))
            folder.name = folder.path.substring(folder.path
                    .lastIndexOf(File.separator) + 1)
            list.add(folder)
        }
        cursor.close()
        return list
    }
}

我们可以看到,使用DaoFactory.getDao拿到dao对象就可以以ORM的方式操作数据库中的表了。

MusicControl媒体控制的具体实现

package site.doramusic.app.media;

import android.content.Context;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.audiofx.BassBoost;
import android.media.audiofx.Equalizer;
import android.os.Build;
import android.os.PowerManager;

import com.lsxiao.apollo.core.Apollo;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import dora.db.builder.WhereBuilder;
import dora.db.dao.DaoFactory;
import dora.db.dao.OrmDao;
import dora.util.LogUtils;
import dora.util.TextUtils;
import dora.util.ToastUtils;
import site.doramusic.app.base.conf.ApolloEvent;
import site.doramusic.app.base.conf.AppConfig;
import site.doramusic.app.db.Music;
import site.doramusic.app.util.PreferencesManager;

/**
 * 音乐播放流程控制。
 */
public class MusicControl implements MediaPlayer.OnCompletionListener, AppConfig {

    private final Random mRandom;
    private int mPlayMode;
    private final MediaPlayerProxy mMediaPlayer;
    private final List<Music> mPlaylist;
    private final Context mContext;
    private int mCurPlayIndex;
    private int mPlayState;
    private int mPendingProgress;
    private final int mCurMusicId;
    private Music mCurMusic;
    private boolean mPlaying;
    private final AudioManager mAudioManager;
    private final OrmDao<Music> mDao;
    private final PreferencesManager mPrefsManager;

    public MusicControl(Context context) {
        this.mContext = context;
        this.mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        this.mPrefsManager = new PreferencesManager(context);
        this.mPlayMode = MPM_LIST_LOOP_PLAY;    //默认列表循环
        this.mPlayState = MPS_NO_FILE;  //默认没有音频文件播放
        this.mCurPlayIndex = -1;
        this.mCurMusicId = -1;
        this.mPlaylist = new ArrayList<>();
        this.mDao = DaoFactory.INSTANCE.getDao(Music.class);
        this.mMediaPlayer = new MediaPlayerProxy();
        this.mMediaPlayer.setNeedCacheAudio(true);
        this.mMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); //播放音频的时候加锁,防止CPU休眠
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            AudioAttributes attrs = new AudioAttributes.Builder()
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build();
            this.mMediaPlayer.setAudioAttributes(attrs);
        } else {
            this.mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        }
        this.mMediaPlayer.setOnCompletionListener(this);
        this.mRandom = new Random();
        this.mRandom.setSeed(System.currentTimeMillis());
    }

    /**
     * 设置重低音参数。
     *
     * @param strength
     */
    public void setBassBoost(int strength) {
        int audioSessionId = mMediaPlayer.getAudioSessionId();
        BassBoost bassBoost = new BassBoost(0, audioSessionId);
        BassBoost.Settings settings = new BassBoost.Settings();
        settings.strength = (short) strength;
        bassBoost.setProperties(settings);
        bassBoost.setEnabled(true);
        bassBoost.setParameterListener(new BassBoost.OnParameterChangeListener() {
            @Override
            public void onParameterChange(BassBoost effect, int status, int param, short value) {
                LogUtils.i("重低音参数改变");
            }
        });
    }

    /**
     * 获取均衡器支持的频率。
     *
     * @return
     */
    public int[] getEqualizerFreq() {
        int audioSessionId = mMediaPlayer.getAudioSessionId();
        Equalizer equalizer = new Equalizer(0, audioSessionId);
        short bands = equalizer.getNumberOfBands();
        int[] freqs = new int[bands];
        for (short i = 0; i < bands; i++) {
            int centerFreq = equalizer.getCenterFreq(i) / 1000;
            freqs[i] = centerFreq;
        }
        return freqs;
    }

    /**
     * 设置均衡器。
     *
     * @param bandLevels
     */
    public void setEqualizer(int[] bandLevels) {
        int audioSessionId = mMediaPlayer.getAudioSessionId();
        Equalizer equalizer = new Equalizer(1, audioSessionId);
        // 获取均衡控制器支持最小值和最大值
        short minEQLevel = equalizer.getBandLevelRange()[0];//第一个下标为最低的限度范围
        short maxEQLevel = equalizer.getBandLevelRange()[1];  // 第二个下标为最高的限度范围
        int distanceEQLevel = maxEQLevel - minEQLevel;
        int singleEQLevel = distanceEQLevel / 25;
        for (short i = 0; i < bandLevels.length; i++) {
            equalizer.setBandLevel(i, (short) (singleEQLevel * bandLevels[i]));
        }
        equalizer.setEnabled(true);
        equalizer.setParameterListener(new Equalizer.OnParameterChangeListener() {
            @Override
            public void onParameterChange(Equalizer effect, int status, int param1, int param2, int value) {
                LogUtils.i("均衡器参数改变:" + status + "," + param1 + "," + param2 + "," + value);
            }
        });
    }

    /**
     * 保存收藏。
     *
     * @param music
     */
    private void saveFavorite(Music music) {
        music.favorite = 1;
        mDao.update(WhereBuilder.Companion.create().addWhereEqualTo("_id", music.id), music);
    }

    /**
     * 保存最近播放。
     *
     * @param music
     */
    private void saveLatest(Music music) {
        //更新本地缓存歌曲
        music.lastPlayTime = System.currentTimeMillis();
        mDao.update(WhereBuilder.Companion.create().addWhereEqualTo("_id", music.id), music);
    }

    /**
     * 设置播放。
     *
     * @param playState
     */
    public void setPlaying(int playState) {
        switch (playState) {
            case MPS_PLAYING:
                mPlaying = true;
                break;
            default:
                mPlaying = false;
        }
    }

    /**
     * 设置当前播放的歌曲。
     *
     * @param music
     * @return
     */
    public boolean loadCurMusic(Music music) {
        if (prepare(seekPosById(mPlaylist, music.songId))) {
            this.mCurMusic = music;
            return true;
        }
        return false;
    }

    /**
     * 修改当前播放歌曲的信息。
     *
     * @param music
     * @return
     */
    public void setCurMusic(Music music) {
        this.mPlaylist.set(mCurPlayIndex, music);
        this.mCurMusic = music;
    }

    /**
     * 缓冲准备。
     *
     * @param pos
     * @return
     */
    public boolean prepare(int pos) {
        mCurPlayIndex = pos;
        mPendingProgress = 0;
        mMediaPlayer.reset();
        if (mPrefsManager.getBassBoost()) {
            setBassBoost(1000);
        } else {
            setBassBoost(1);
        }
        if (!mPrefsManager.getEqualizerDecibels().equals("")) {
            int[] equalizerFreq = getEqualizerFreq();
            int[] decibels = new int[equalizerFreq.length];
            String[] values = mPrefsManager.getEqualizerDecibels().split(",");
            for (int i = 0; i < decibels.length; i++) {
                decibels[i] = Integer.valueOf(values[i]);
            }
            setEqualizer(decibels);
        }
        String path = mPlaylist.get(pos).data;
        if (TextUtils.isNotEmpty(path)) {
            try {
                mMediaPlayer.setDataSource(path);
                mMediaPlayer.prepare();
                mPlayState = MPS_PREPARE;
            } catch (Exception e) {
                mPlayState = MPS_INVALID;
                if (pos < mPlaylist.size()) {
                    pos++;
                    playById(mPlaylist.get(pos).songId);
                }
                return false;
            }
        } else {
            ToastUtils.showShort(mContext, "歌曲路径为空");
        }
        mCurMusic = mPlaylist.get(mCurPlayIndex);
        sendMusicPlayBroadcast();
        return true;
    }

    /**
     * 根据歌曲的id来播放。
     *
     * @param id
     * @return
     */
    public boolean playById(int id) {
        if (requestFocus()) {
            int position = seekPosById(mPlaylist, id);
            mCurPlayIndex = position;
            if (mCurMusicId == id) {
                if (!mMediaPlayer.isPlaying()) {
                    mMediaPlayer.start();
                    mPlayState = MPS_PLAYING;
                    sendMusicPlayBroadcast();
                    mCurMusic = mPlaylist.get(mCurPlayIndex);
                    saveLatest(mCurMusic);
                } else {
                    pause();
                }
                return true;
            }
            if (!prepare(position)) {
                return false;
            }
            return replay();
        } else {
            return false;
        }
    }

    /**
     * 根据URL播放歌曲。
     *
     * @param music
     * @param url
     */
    public void playByUrl(Music music, String url) {
        if (requestFocus()) {
            try {
                mMediaPlayer.setAudioCachePath(music.data);
                mMediaPlayer.setOnCachedProgressUpdateListener(new MediaPlayerProxy.OnCachedProgressUpdateListener() {
                    @Override
                    public void updateCachedProgress(int progress) {
                        mPendingProgress = progress;
                    }
                });
                String localProxyUrl = mMediaPlayer.getLocalURLAndSetRemoteSocketAddress(url);
                mPlaylist.add(mCurPlayIndex, music);    //插入到当前播放位置
                mCurMusic = music;
                mMediaPlayer.startProxy();
                mMediaPlayer.reset();
                mMediaPlayer.setDataSource(localProxyUrl);
                mMediaPlayer.prepareAsync();
                mMediaPlayer.start();
                mPlayState = MPS_PLAYING;
                sendMusicPlayBroadcast();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 根据本地文件路径播放歌曲。
     *
     * @param path
     */
    public void play(String path) {
        if (requestFocus()) {
            try {
                mMediaPlayer.stop();
                mMediaPlayer.reset();
                mMediaPlayer.setDataSource(path);
                mMediaPlayer.prepare();
                mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                    @Override
                    public void onPrepared(MediaPlayer mp) {
                        mMediaPlayer.start();
                        sendMusicPlayBroadcast();
                    }
                });
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 停止播放歌曲。
     */
    public void stop() {
        if (mMediaPlayer.isPlaying()) {
            mMediaPlayer.stop();
        }
    }

    AudioManager.OnAudioFocusChangeListener audioFocusListener = new AudioManager.OnAudioFocusChangeListener() {
        public void onAudioFocusChange(int focusChange) {
            if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
                // Pause playback
                pause();
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                // Resume playback
                replay();
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                mAudioManager.abandonAudioFocus(audioFocusListener);
                pause();
            }
        }
    };

    /**
     * 请求音频焦点。
     *
     * @return
     */
    private boolean requestFocus() {
        // Request audio focus for playback
        int result = mAudioManager.requestAudioFocus(audioFocusListener,
                // Use the music stream.
                AudioManager.STREAM_MUSIC,
                // Request permanent focus.
                AudioManager.AUDIOFOCUS_GAIN);
        return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
    }

    /**
     * 根据位置播放列表中的歌曲。
     *
     * @param pos
     * @return
     */
    public boolean play(int pos) {
        if (requestFocus()) {
            if (mCurPlayIndex == pos) {
                if (!mMediaPlayer.isPlaying()) {
                    mMediaPlayer.start();
                    mPlayState = MPS_PLAYING;
                    sendMusicPlayBroadcast();
                    mCurMusic = mPlaylist.get(mCurPlayIndex);
                    saveLatest(mCurMusic);
                } else {
                    pause();
                }
                return true;
            }
            if (!prepare(pos)) {
                return false;
            }
            return replay();
        } else {
            return false;
        }
    }

    /**
     * 获取当前播放歌曲的索引。
     *
     * @return
     */
    public int getCurPlayIndex() {
        return mCurPlayIndex;
    }

    /**
     * 保证索引在播放列表索引范围内。
     *
     * @param index
     * @return
     */
    private int reviseIndex(int index) {
        if (index < 0) {
            index = mPlaylist.size() - 1;
        }
        if (index >= mPlaylist.size()) {
            index = 0;
        }
        return index;
    }

    /**
     * 获取当前歌曲播放的位置。
     *
     * @return
     */
    public int position() {
        if (mPlayState == MPS_PLAYING || mPlayState == MPS_PAUSE) {
            return mMediaPlayer.getCurrentPosition();
        }
        return 0;
    }

    /**
     * 获取当前歌曲的时长。
     *
     * @return 毫秒
     */
    public int duration() {
        if (mPlayState == MPS_INVALID || mPlayState == MPS_NO_FILE) {
            return 0;
        }
        return mMediaPlayer.getDuration();
    }

    /**
     * 跳到指定进度播放歌曲。
     *
     * @param progress
     * @return
     */
    public boolean seekTo(int progress) {
        if (mPlayState == MPS_INVALID || mPlayState == MPS_NO_FILE) {
            return false;
        }
        int pro = reviseSeekValue(progress);
        int time = mMediaPlayer.getDuration();
        int curTime = (int) ((float) pro / 100 * time);
        mMediaPlayer.seekTo(curTime);
        return true;
    }

    /**
     * 获取歌曲的播放模式。
     *
     * @return
     */
    public int getPlayMode() {
        return mPlayMode;
    }

    /**
     * 设置歌曲的播放模式。
     *
     * @param mode
     */
    public void setPlayMode(int mode) {
        this.mPlayMode = mode;
    }

    /**
     * 清空播放列表。
     */
    public void clear() {
        mMediaPlayer.stop();
        mMediaPlayer.reset();
    }

    /**
     * 在线缓冲进度。
     *
     * @return
     */
    public int pendingProgress() {
        return mPendingProgress;
    }

    public interface OnConnectCompletionListener {

        void onConnectCompletion(IMediaService service);
    }

    /**
     * 获取当前正在播放的歌曲。
     *
     * @return
     */
    public Music getCurMusic() {
        return mCurMusic;
    }

    /**
     * 检测当前歌曲是否正在播放中。
     *
     * @return
     */
    public boolean isPlaying() {
        return mPlaying;
    }

    /**
     * 暂停当前歌曲的播放。
     *
     * @return
     */
    public boolean pause() {
        if (mPlayState != MPS_PLAYING) {
            return false;
        }
        mMediaPlayer.pause();
        mPlayState = MPS_PAUSE;
        mCurMusic = mPlaylist.get(mCurPlayIndex);
        sendMusicPlayBroadcast();
        return true;
    }

    /**
     * 播放上一首。
     *
     * @return
     */
    public boolean prev() {
        switch (mPlayMode) {
            case AppConfig.MPM_LIST_LOOP_PLAY:    //列表循环
                return moveLeft();
            case AppConfig.MPM_ORDER_PLAY:    //顺序播放
                if (mCurPlayIndex != 0) {
                    return moveLeft();
                } else {
                    return prepare(mCurPlayIndex);
                }
            case AppConfig.MPM_RANDOM_PLAY:   //随机播放
                int index = getRandomIndex();
                if (index != -1) {
                    mCurPlayIndex = index;
                } else {
                    mCurPlayIndex = 0;
                }
                if (prepare(mCurPlayIndex)) {
                    return replay();
                }
                return false;
            case AppConfig.MPM_SINGLE_LOOP_PLAY:  //单曲循环
                prepare(mCurPlayIndex);
                return replay();
                default:
                    return false;
        }
    }

    /**
     * 播放下一首。
     *
     * @return
     */
    public boolean next() {
        switch (mPlayMode) {
            case MPM_LIST_LOOP_PLAY:    //列表循环
                return moveRight();
            case MPM_ORDER_PLAY:    //顺序播放
                if (mCurPlayIndex != mPlaylist.size() - 1) {
                    return moveRight();
                } else {
                    return prepare(mCurPlayIndex);
                }
            case MPM_RANDOM_PLAY:   //随机播放
                int index = getRandomIndex();
                if (index != -1) {
                    mCurPlayIndex = index;
                } else {
                    mCurPlayIndex = 0;
                }
                if (prepare(mCurPlayIndex)) {
                    return replay();
                }
                return false;
            case MPM_SINGLE_LOOP_PLAY:  //单曲循环
                prepare(mCurPlayIndex);
                return replay();
                default:
                return false;
        }
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        next();
    }

    /**
     * 随机播放模式下获取播放索引。
     *
     * @return
     */
    private int getRandomIndex() {
        int size = mPlaylist.size();
        if (size == 0) {
            return -1;
        }
        return Math.abs(mRandom.nextInt() % size);
    }

    /**
     * 修正缓冲播放的进度在合理的范围内。
     *
     * @param progress
     * @return
     */
    private int reviseSeekValue(int progress) {
        if (progress < 0) {
            progress = 0;
        } else if (progress > 100) {
            progress = 100;
        }
        return progress;
    }

    /**
     * 刷新播放列表的歌曲。
     *
     * @param playlist
     */
    public void refreshPlaylist(List<Music> playlist) {
        mPlaylist.clear();
        mPlaylist.addAll(playlist);
        if (mPlaylist.size() == 0) {
            mPlayState = MPS_NO_FILE;
            mCurPlayIndex = -1;
            return;
        }
    }

    /**
     * 在当前播放模式下播放上一首。
     *
     * @return
     */
    public boolean moveLeft() {
        if (mPlayState == MPS_NO_FILE) {
            return false;
        }
        mCurPlayIndex--;
        mCurPlayIndex = reviseIndex(mCurPlayIndex);
        if (!prepare(mCurPlayIndex)) {
            return false;
        }
        return replay();
    }

    /**
     * 在当前播放模式下播放下一首。
     *
     * @return
     */
    public boolean moveRight() {
        if (mPlayState == MPS_NO_FILE) {
            return false;
        }
        mCurPlayIndex++;
        mCurPlayIndex = reviseIndex(mCurPlayIndex);
        if (!prepare(mCurPlayIndex)) {
            return false;
        }
        return replay();
    }

    /**
     * 重头开始播放当前歌曲。
     *
     * @return
     */
    public boolean replay() {
        if (requestFocus()) {
            if (mPlayState == MPS_INVALID || mPlayState == MPS_NO_FILE) {
                return false;
            }
            mMediaPlayer.start();
            mPlayState = MPS_PLAYING;
            sendMusicPlayBroadcast();
            mCurMusic = mPlaylist.get(mCurPlayIndex);
            saveLatest(mCurMusic);
            return true;
        } else {
            return false;
        }
    }

    /**
     * 发送音乐播放/暂停的广播。
     */
    private void sendMusicPlayBroadcast() {
        setPlaying(mPlayState);
        Intent intent = new Intent(ACTION_PLAY);
        intent.putExtra("play_state", mPlayState);
        mContext.sendBroadcast(intent);
        Apollo.emit(ApolloEvent.REFRESH_LOCAL_NUMS);
    }

    /**
     * 获取当前的播放状态。
     *
     * @return
     */
    public int getPlayState() {
        return mPlayState;
    }

    /**
     * 获取播放列表。
     *
     * @return
     */
    public List<Music> getPlaylist() {
        return mPlaylist;
    }

    /**
     * 退出媒体播放。
     */
    public void exit() {
        mMediaPlayer.stop();
        mMediaPlayer.release();
        mCurPlayIndex = -1;
        mPlaylist.clear();
    }

    /**
     * 根据歌曲的ID,寻找出歌曲在当前播放列表中的位置。
     *
     * @param playlist
     * @param id
     * @return
     */
    public int seekPosById(List<Music> playlist, int id) {
        if (id == -1) {
            return -1;
        }
        int result = -1;
        if (playlist != null) {

            for (int i = 0; i < playlist.size(); i++) {
                if (id == playlist.get(i).songId) {
                    result = i;
                    break;
                }
            }
        }
        return result;
    }
}

前面我们提到使用AIDL进行跨进程访问。那么整体调用顺序是,MediaManager->MediaService->MusicControl。MediaManager调用层,相当于一个外包装或者说是门面。MediaService中间层,用于后台访问。MusicControl实现层。

ShakeDetector摇一摇切歌

package site.doramusic.app.shake

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Handler

import site.doramusic.app.util.PreferencesManager

/**
 * 摇一摇切歌。
 */
class ShakeDetector(context: Context) : SensorEventListener {

    private val sensorManager: SensorManager?
    private var onShakeListener: OnShakeListener? = null
    private val prefsManager: PreferencesManager
    private var lowX: Float = 0.toFloat()
    private var lowY: Float = 0.toFloat()
    private var lowZ: Float = 0.toFloat()
    private var shaking: Boolean = false
    private val shakeHandler: Handler by lazy {
        Handler()
    }

    companion object {
        private const val FILTERING_VALUE = 0.1f
    }

    init {
        // 获取传感器管理服务
        sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
        prefsManager = PreferencesManager(context)
    }

    private val r: Runnable = Runnable {
        shaking = false
    }

    override fun onSensorChanged(event: SensorEvent) {
        if (prefsManager.getShakeChangeMusic() && event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
            if (!shaking) {
                shakeHandler.removeCallbacks(r)
                val x = event.values[SensorManager.DATA_X]
                val y = event.values[SensorManager.DATA_Y]
                val z = event.values[SensorManager.DATA_Z]
                lowX = x * FILTERING_VALUE + lowX * (1.0f - FILTERING_VALUE)
                lowY = y * FILTERING_VALUE + lowY * (1.0f - FILTERING_VALUE)
                lowZ = z * FILTERING_VALUE + lowZ * (1.0f - FILTERING_VALUE)
                val highX = x - lowX
                val highY = y - lowY
                val highZ = z - lowZ
                if (highX >= 10 || highY >= 10 || highZ >= 10) {
                    shaking = true
                    onShakeListener?.onShake()
                    shakeHandler.postDelayed(r, 2000)
                }
            }
        }
    }

    override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
        //传感器精度改变
    }

    /**
     * 启动摇晃检测--注册监听器。
     */
    fun start() {
        sensorManager?.registerListener(this,
                sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
                SensorManager.SENSOR_DELAY_NORMAL)
    }

    /**
     * 停止摇晃检测--取消监听器。
     */
    fun stop() {
        sensorManager?.unregisterListener(this)
    }

    /**
     * 当摇晃事件发生时,接收通知。
     */
    interface OnShakeListener {

        /**
         * 当手机晃动时被调用。
         */
        fun onShake()
    }

    fun setOnShakeListener(l: OnShakeListener) {
        this.onShakeListener = l
    }
}

摇一摇功能的实现原理很简单,就是使用了Android的重力传感器,当x,y,z轴的加速度超过了预先设定的阈值,就会触发摇一摇功能,我们这里是调用MediaManager播放下一首歌。因为MediaManager管理着整个可以播放的音乐列表,所以随时都可以触发摇一摇功能,当然在设置中关掉了摇一摇功能除外。

拔出耳机或断开蓝牙耳机连接暂停播放音乐

package site.doramusic.app.receiver

import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothProfile
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.media.AudioManager
import android.os.Handler

import site.doramusic.app.MusicApp
import site.doramusic.app.R
import site.doramusic.app.media.SimpleAudioPlayer

/**
 * 耳机拨出监听。
 */
class EarphoneReceiver : BroadcastReceiver() {

    private lateinit var player: SimpleAudioPlayer

    override fun onReceive(context: Context, intent: Intent) {
        val action = intent.action
        if (action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
            changeSpeakerphoneOn(context, true)
            // 只监听拔出耳机使用这个意图
            // 耳机拔出时,暂停音乐播放
            Handler().postDelayed({
                player = SimpleAudioPlayer(context)
                player.playByRawId(R.raw.earphone)
            }, 1000)
            pauseMusic()
        } else if (Intent.ACTION_HEADSET_PLUG == action) {
            //            if (intent.hasExtra("state")) {
            //                int state = intent.getIntExtra("state", -1);
            //                if (state == 1) {
            //                    //插入耳机
            //                } else if (state == 0) {
            //                    //拔出耳机
            //                    pauseMusic();
            //                }
            //            }
        } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED == action) {
            val adapter = BluetoothAdapter.getDefaultAdapter()
            if (BluetoothProfile.STATE_DISCONNECTED == adapter.getProfileConnectionState(BluetoothProfile.A2DP) ||
                    BluetoothProfile.STATE_DISCONNECTED == adapter.getProfileConnectionState(BluetoothProfile.HEADSET) ||
                    BluetoothProfile.STATE_DISCONNECTED == adapter.getProfileConnectionState(BluetoothProfile.HEALTH) ||
                    BluetoothProfile.STATE_DISCONNECTED == adapter.getProfileConnectionState(BluetoothProfile.GATT)) {
                changeSpeakerphoneOn(context, true)
                //蓝牙耳机失去连接
                Handler().postDelayed({
                    player = SimpleAudioPlayer(context)
                    player.playByRawId(R.raw.bluetooth)
                }, 1000)
                pauseMusic()
            } else if (BluetoothProfile.STATE_CONNECTED == adapter.getProfileConnectionState(BluetoothProfile.HEADSET) ||
                    BluetoothProfile.STATE_CONNECTED == adapter.getProfileConnectionState(BluetoothProfile.HEADSET) ||
                    BluetoothProfile.STATE_CONNECTED == adapter.getProfileConnectionState(BluetoothProfile.HEALTH) ||
                    BluetoothProfile.STATE_CONNECTED == adapter.getProfileConnectionState(BluetoothProfile.GATT)) {
                //蓝牙耳机已连接
            }
        }
    }

    private fun pauseMusic() {
        MusicApp.instance!!.mediaManager!!.pause()
    }

    /**
     * 切换播放模式。
     *
     * @param connected
     */
    private fun changeSpeakerphoneOn(context: Context, connected: Boolean) {
        val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
        am.isSpeakerphoneOn = connected
    }
}

我们通过监听系统广播来实现这样的功能。

MusicTimer全局音乐播放界面刷新

package site.doramusic.app.util;

import android.os.Handler;
import android.os.Message;

import java.util.Timer;
import java.util.TimerTask;

public class MusicTimer {

    public final static int REFRESH_PROGRESS_EVENT = 0x100;
    private static final int INTERVAL_TIME = 500;
    private Handler[] mHandler;
    private Timer mTimer;
    private TimerTask mTimerTask;

    private int what;
    private boolean mTimerStart = false;

    public MusicTimer(Handler... handler) {
        this.mHandler = handler;
        this.what = REFRESH_PROGRESS_EVENT;
        mTimer = new Timer();
    }

    public void startTimer() {
        if (mHandler == null || mTimerStart) {
            return;
        }
        mTimerTask = new MusicTimerTask();
        mTimer.schedule(mTimerTask, INTERVAL_TIME, INTERVAL_TIME);
        mTimerStart = true;
    }

    public void stopTimer() {
        if (!mTimerStart) {
            return;
        }
        mTimerStart = false;
        if (mTimerTask != null) {
            mTimerTask.cancel();
            mTimerTask = null;
        }
    }

    class MusicTimerTask extends TimerTask {

        @Override
        public void run() {
            if (mHandler != null) {
                for (Handler handler : mHandler) {
                    Message msg = handler.obtainMessage(what);
                    msg.sendToTarget();
                }
            }
        }
    }
}

我们所有需要刷新进度条的地方都要用到这个类,一般设置为0.5秒刷新一次,既不过度刷新,又要保证歌曲时间播放进度较准确。

BaseActivity写法体验

package site.doramusic.app.ui.activity

import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.ViewGroup
import android.widget.RelativeLayout
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import com.alibaba.android.arouter.facade.annotation.Route
import dora.skin.SkinManager
import dora.skin.base.BaseSkinActivity
import dora.util.DensityUtils
import dora.util.StatusBarUtils
import dora.widget.DoraTitleBar
import site.doramusic.app.R
import site.doramusic.app.annotation.TimeTrace
import site.doramusic.app.base.conf.ARoutePath
import site.doramusic.app.databinding.ActivityChoiceColorBinding
import site.doramusic.app.ui.adapter.ChoiceColorAdapter
import site.doramusic.app.util.PreferencesManager

/**
 * 换肤界面,选择颜色。
 */
@Route(path = ARoutePath.ACTIVITY_CHOICE_COLOR)
class ChoiceColorActivity : BaseSkinActivity<ActivityChoiceColorBinding>() {

    private lateinit var colorDrawable: ColorDrawable
    private var choiceColorAdapter: ChoiceColorAdapter? = null
    private var colorDatas: MutableList<ColorData>? = null
    private lateinit var prefsManager: PreferencesManager

    data class ColorData(val backgroundResId: Int, val backgroundColor: Int)

    override fun getLayoutId(): Int {
        return R.layout.activity_choice_color
    }

    override fun onSetStatusBar() {
        super.onSetStatusBar()
        StatusBarUtils.setTransparencyStatusBar(this)
    }

    override fun initData(savedInstanceState: Bundle?) {
        mBinding.statusbarChoiceColor.layoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            StatusBarUtils.getStatusBarHeight())
        SkinManager.getLoader().setBackgroundColor(mBinding.statusbarChoiceColor, "skin_theme_color")
        val imageView = AppCompatImageView(this)
        val dp24 = DensityUtils.dp2px(24f)
        imageView.layoutParams = RelativeLayout.LayoutParams(dp24, dp24)
        imageView.setImageResource(R.drawable.ic_save)
        mBinding.titlebarChoiceColor.addMenuButton(imageView)

        mBinding.titlebarChoiceColor.setOnIconClickListener(object : DoraTitleBar.OnIconClickListener {
            override fun onIconBackClick(icon: AppCompatImageView) {
            }

            override fun onIconMenuClick(position: Int, icon: AppCompatImageView) {
                if (position == 0) {
                    changeSkin()
                }
            }
        })
        prefsManager = PreferencesManager(this)
        colorDatas = mutableListOf(
            ColorData(R.drawable.cyan_bg,
                resources.getColor(R.color.skin_theme_color_cyan)),
            ColorData(R.drawable.orange_bg,
                resources.getColor(R.color.skin_theme_color_orange)),
            ColorData(R.drawable.black_bg,
                resources.getColor(R.color.skin_theme_color_black)),
            ColorData(R.drawable.green_bg,
                resources.getColor(R.color.skin_theme_color_green)),
            ColorData(R.drawable.red_bg,
                resources.getColor(R.color.skin_theme_color_red)),
            ColorData(R.drawable.blue_bg,
                resources.getColor(R.color.skin_theme_color_blue)),
            ColorData(R.drawable.purple_bg,
                resources.getColor(R.color.skin_theme_color_purple)))

        choiceColorAdapter = ChoiceColorAdapter()
        choiceColorAdapter!!.setList(colorDatas!!)
        mBinding.rvChoiceColor.layoutManager = LinearLayoutManager(this,
            LinearLayoutManager.HORIZONTAL, false)
//        mBinding.rvChoiceColor.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL))
        mBinding.rvChoiceColor.itemAnimator = DefaultItemAnimator()
        mBinding.rvChoiceColor.adapter = choiceColorAdapter
        choiceColorAdapter!!.selectedPosition = if (prefsManager.getSkinType() == 0) 0 else prefsManager.getSkinType() - 1

        colorDrawable = ColorDrawable(ContextCompat.getColor(this, R.color.colorPrimary))
        mBinding.ivChoiceColorPreview.background = colorDrawable
        choiceColorAdapter!!.setOnItemClickListener { adapter, view, position ->
            val color = colorDatas!![position].backgroundColor
            colorDrawable.color = color
            choiceColorAdapter!!.selectedPosition = position
            choiceColorAdapter!!.notifyDataSetChanged()
        }
    }

    /**
     * 测试AOP。
     */
    @TimeTrace
    private fun changeSkin() {
        when (choiceColorAdapter!!.selectedPosition) {
            0 -> {
                prefsManager.saveSkinType(1)
                SkinManager.changeSkin("cyan")
            }
            1 -> {
                prefsManager.saveSkinType(2)
                SkinManager.changeSkin("orange")
            }
            2 -> {
                prefsManager.saveSkinType(3)
                SkinManager.changeSkin("black")
            }
            3 -> {
                prefsManager.saveSkinType(4)
                SkinManager.changeSkin("green")
            }
            4 -> {
                prefsManager.saveSkinType(5)
                SkinManager.changeSkin("red")
            }
            5 -> {
                prefsManager.saveSkinType(6)
                SkinManager.changeSkin("blue")
            }
            6 -> {
                prefsManager.saveSkinType(7)
                SkinManager.changeSkin("purple")
            }
        }
        SkinManager.getLoader().setBackgroundColor(mBinding.statusbarChoiceColor, "skin_theme_color")
        finish()
    }
}

以换肤界面为例,另外换肤可以看我这篇文章我的又一个神奇的框架——Skins换肤框架 ,我这里就不多说了。使用dora.BaseActivity和dora.BaseFragment,可以统一数据加载都在initData中。

开源项目地址

https://github.com/dora4/DoraMusic

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1710631.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入探究Java自动拆箱与装箱的实现原理

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

浙江大学数据结构MOOC-课后习题-第九讲-排序2 Insert or Merge

题目汇总 浙江大学数据结构MOOC-课后习题-拼题A-代码分享-2024 题目描述 测试点 思路分析 刚开始我打算想推出一个规律&#xff0c;来判断是否是归并排序&#xff0c;但实在太过于复杂&#xff0c;我很难去想出这样的规律…因此&#xff0c;参考了其他博主的思路——每做一次排…

AI菜鸟向前飞 — LangChain系列之十五 - Agent系列:从现象看机制(中篇)一个Agent的“旅行”

Agent基本架构 先谈谈Agent基本架构概念&#xff0c;如果看得云里雾里&#xff0c;等看完本篇之后&#xff0c;再回头看就会豁然开朗的&#xff0c;而我尽量写得更易懂&#xff1a; &#xff09; 这里面会穿插着上一篇的内容&#xff0c;请大家记得往回翻翻&#xff0c;传送门&…

Vue.js中如何实现以列表首列为表头

前言 一般情况table列表的展示&#xff0c;列头都在第一横行&#xff0c;此方法用于列头在第一列的情况。 效果图 核心代码 <template><div><table class"data-table"><tr v-for"(column, columnIndex) in columns" :key"col…

maven部署到私服

方法一:网页上传 1、账号登录 用户名/密码 2、地址 http://自己的ip:自己的端口/nexus 3、查看Repositories列表&#xff0c;选择Public Repositories&#xff0c;确定待上传jar包不在私服中 4、选择3rd party仓库&#xff0c;点击Artifact Upload页签 5、GAV Definition选…

MagicPose4D:解锁AI驱动的3D模型动作新纪元

在当今快速发展的数字内容创作领域,MagicPose4D正以其革命性的技术颠覆传统动画制作流程,成为创作者手中的魔法棒。这款先进的框架不仅仅是一款工具,更是通往无限创意的一扇门,它使得为3D模型赋予生动、自然的动作变得前所未有的简单和高效。下面,让我们深入探索MagicPose…

STM32高级控制定时器(STM32F103):检测输入PWM周期和占空比

目录 概述 1 PWM 输入模式 1.1 原理介绍 1.2 应用实例 1.3 示例时序图 2 使用STM32Cube配置工程 2.1 软件环境 2.2 配置参数 2.3 生成项目文件 3 功能实现 3.1 PWM占空比函数 3.2 输入捕捉回调函数 4 功能测试 4.1 测试软件框架结构 4.2 实验实现 4.2.1 测试实…

Redis介绍及安装配置

1 什么是Redis Redis 的定义&#xff1a;Redis&#xff08;Remote Dictionary Server 远程字典服务&#xff09;是一个开源的使用C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库&#xff0c;并提供多种语言的API。 可见Redis和我们最经常使用的MySQL都…

动态规划之买卖股票大集合

目录 引言 1.只能进行一次买卖股票&#xff08;最多只能买一股股票&#xff09; 2.可以进行多次股票买卖&#xff0c;且没有手续费&#xff08;最多只能买一股股票&#xff09; 3.可以进行多次股票买卖&#xff0c;但是有冷冻期&#xff0c;无手续费&#xff08;最多只能买一…

Firefox浏览器网页上的按钮点击无效解决办法

我在github下点下载经常不好使&#xff0c;查了原因&#xff0c;原来是浏览器的问题。在Firefox浏览器的设置里面&#xff0c;去掉一些cookies的禁用即可。之后&#xff0c;就可以点击按钮成功响应了。

智能跳绳的产品体验与思考(一)

我&#xff0c;虽称不上跳绳高手&#xff0c;却对这项运动怀有深厚的热爱&#xff0c;也曾在某电商平台上选购过一款智能跳绳&#xff0c;希望能借此提升我的跳绳技巧。今天&#xff0c;咱们就来聊聊我和这条绳子的发生的一些故事&#xff0c;外加我的一些思考。 此刻&#xf…

NDIS协议驱动(四)

NDIS 定义对象标识符 (OID) 值&#xff0c;以标识适配器参数&#xff0c;其中包括设备特征、可配置设置和统计信息等操作参数。 协议驱动程序可以查询或设置基础驱动程序的操作参数。 NDIS 还为 NDIS 6.1 及更高版本的协议驱动程序提供直接 OID 请求接口。 直接 OID 请求路径支…

Java-文件操作

一、创建文件 1.创建文件夹 创建文件夹时&#xff0c;注意两个条件&#xff0c;该路径对应的是否为目录&#xff08;dir&#xff09;&#xff0c;该文件夹是否存在。 File Apathnew File("./文件夹A"); //当前路径文件夹的存储路径if(!Apath.exists() &&am…

【研0深度学习】李宏毅2024春《生成式人工智能导论》持续更新...

文章目录 第1讲 什么是生成式人工智慧&#xff1f;第2讲 今日的生成式人工智慧厉害在哪里&#xff1f;第3-5讲 训练不了人工智慧&#xff0c;你可以训练你自己&#xff08;在不训练模型的情况下强化语言模型的方法&#xff09;第6讲 大模型修炼史——第一阶段 自我学习 累计实力…

ROS2入门21讲__第08讲__话题:节点间传递数据的桥梁

目录 前言 通信模型 发布/订阅模型 多对多通信 异步通信 消息接口 案例一&#xff1a;Hello World话题通信 运行效果 发布者代码解析 程序实现 流程总结 订阅者代码解析 程序实现 流程总结 案例二&#xff1a;机器视觉识别 运行效果 发布者代码解析 订阅者代…

WebGL学习(一)渲染关系

学习webgl 开发理解渲染关系是必须的&#xff0c;也非常重要&#xff0c;很多人忽视了这个过程。 我这里先简单写一下&#xff0c;后面尽量用通俗易懂的方式&#xff0c;举例讲解。 WebGL&#xff0c;全称Web Graphics Library&#xff0c;是一种在网页上渲染3D图形的技术。它…

FPGA时钟:驱动数字逻辑的核心

一、引言 在FPGA&#xff08;现场可编程门阵列&#xff09;设计中&#xff0c;时钟信号是不可或缺的关键要素。时钟信号作为时序逻辑的心跳&#xff0c;推动着FPGA内部各个存储单元的数据流转。无论是实现复杂的逻辑运算还是处理高速数据流&#xff0c;都需要精确的时钟信号来保…

CASS11自定义宗地图框

1、找到CASS11的安装路径&#xff0c;找到如下文件夹&#xff1a; 2、打开【report】文件夹&#xff0c;如下&#xff1a; 3、打开其中一个压缩包&#xff0c;如【标准宗地图】压缩包&#xff0c;结果如下&#xff1a; 4、打开后&#xff0c;将其另存为到桌面&#xff0c;随后关…

Leetcode621. 任务调度器

Every day a Leetcode 题目来源&#xff1a;621. 任务调度器 类似题目&#xff1a;1953. 你可以工作的最大周数 解法1&#xff1a;贪心 本质上来说&#xff0c;我们需要构造一个尽量短的&#xff0c;相同元素间隔 > (n1) 的序列。 用一个数组 cnt 统计每个任务的次数。…

Oracle创建用户时提示ORA-65096:公用用户名或角色名无效

Oracle创建用户时提示“ORA-65096&#xff1a;公用用户名或角色名无效” 如下图所示&#xff1a; 解决方法&#xff1a;在新增用户名前面加上C##或者c##就可以解决无效问题&#xff0c;具体什么原因还不清楚&#xff0c;需要再研究一下。