Android开发之音乐播放器添加排行需求
Music统计功能需求
1.记录歌曲名称与次数(歌曲播放结束算一次),根据播放次数制作一个排行列表;(开始说要记录歌手,后面debug发现这个字段没有,暂时不记录)
2.记录播放歌曲的时长,时间累加;(经沟通,需要细分成每一个月的播放时长,另外再加一个首次使用的记录)
前几天需要实现这功能,昨天实现了,今天验证Ok,来谈谈我的做法,先上图(暂时版):

上面是一个简单的例子,具体UI后期需要给图再做调整,目前结构上面是一个通用的带返回键的titlebar,下面recyclerview加文本做数据展示,方便测试看数据,当然我都是用sqlite expert去查看数据:



这里需要注意的是当我们从设备导出数据库的时候需要把.db和.db-shm和.db-wal都要导出,不然可能没有表和数据.
1.首先创建音乐播放数据库:
package com.hiby.music.musicinfofetchermaster.db;import static com.hiby.music.musicinfofetchermaster.db.MusicRecordDao.COLUMN_PLAY_COUNT;
import static com.hiby.music.musicinfofetchermaster.db.MusicRecordDao.KEY_DB_TAB;import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;import androidx.annotation.NonNull;
import androidx.annotation.Nullable;public class MusicRecordOpenHelper extends SQLiteOpenHelper {private volatile static MusicRecordOpenHelper instances = null;public static final String DB_NAME = "music_record.db";public static final int DB_VERSION = 1;public static MusicRecordOpenHelper getInstances(Context context) {if (instances == null) {synchronized (MusicRecordOpenHelper.class) {if (instances == null) {instances = new MusicRecordOpenHelper(context.getApplicationContext());}}}return instances;}public MusicRecordOpenHelper(Context context) {super(context, DB_NAME, null, DB_VERSION);}@Overridepublic void onCreate(SQLiteDatabase db) {db.execSQL(MusicRecordDao.CREATE_TABLE_SONGS);}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {db.execSQL("DROP TABLE IF EXISTS " + KEY_DB_TAB);onCreate(db);}public void insertOrUpdateSong(String name, String author) {SQLiteDatabase db = instances.getWritableDatabase();// 查询数据库中是否已经存在相同的歌曲String selection = MusicRecordDao.COLUMN_NAME + " = ? AND " + MusicRecordDao.COLUMN_AUTHOR + " = ?";String[] selectionArgs = {name, author};Cursor cursor = db.query(KEY_DB_TAB,new String[]{COLUMN_PLAY_COUNT},selection,selectionArgs,null,null,null);if (cursor.moveToFirst()) {// 歌曲已存在,更新播放次数int playCount = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_PLAY_COUNT));playCount++; // 增加播放次数ContentValues values = new ContentValues();values.put(COLUMN_PLAY_COUNT, playCount);db.update(KEY_DB_TAB, values, selection, selectionArgs);} else {// 歌曲不存在,插入新记录ContentValues values = new ContentValues();values.put(MusicRecordDao.COLUMN_NAME, name);values.put(MusicRecordDao.COLUMN_AUTHOR, author);values.put(COLUMN_PLAY_COUNT, 1); // 初始播放次数为1,因为这是第一次插入db.insert(KEY_DB_TAB, null, values);}cursor.close();db.close();}// 插入新歌曲public void insertSong(String name, String author) {SQLiteDatabase db = instances.getWritableDatabase();ContentValues values = new ContentValues();values.put(MusicRecordDao.COLUMN_NAME, name);values.put(MusicRecordDao.COLUMN_AUTHOR, author);values.put(COLUMN_PLAY_COUNT, 0); // 初始播放次数为0db.insert(KEY_DB_TAB, null, values);db.close();}// 更新播放次数public void incrementPlayCount(String name, String author) {SQLiteDatabase db = instances.getWritableDatabase();String selection = MusicRecordDao.COLUMN_NAME + " = ? AND " + MusicRecordDao.COLUMN_AUTHOR + " = ?";String[] selectionArgs = {name, author};Cursor cursor = db.query(MusicRecordDao.KEY_DB_TAB, new String[]{COLUMN_PLAY_COUNT},selection, selectionArgs, null, null, null);if (cursor != null && cursor.moveToFirst()) {int playCount = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_PLAY_COUNT));playCount++;ContentValues values = new ContentValues();values.put(COLUMN_PLAY_COUNT, playCount);db.update(MusicRecordDao.KEY_DB_TAB, values, selection, selectionArgs);}if (cursor != null) {cursor.close();}db.close();}// 获取按播放次数排序的音乐记录public Cursor getMusicSortedByPlayCount() {SQLiteDatabase db = this.getReadableDatabase();return db.query(KEY_DB_TAB, null, null, null, null, null, COLUMN_PLAY_COUNT + " DESC");}
}
这里我直接把对数据进行插入的逻辑和查询写在里面了,下面是建表,放在Dao里:
public class MusicRecordDao {public static final String KEY_DB_TAB = "music_record";public static final String COLUMN_ID = "id";public static final String COLUMN_NAME = "name";public static final String COLUMN_AUTHOR = "author";public static final String COLUMN_PLAY_COUNT = "play_count";public static final String CREATE_TABLE_SONGS = "CREATE TABLE " + KEY_DB_TAB + " ("+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "+ COLUMN_NAME + " TEXT NOT NULL, "+ COLUMN_AUTHOR + " TEXT NOT NULL, "+ COLUMN_PLAY_COUNT + " INTEGER DEFAULT 0)";public static final String CLEAN_MUSIC_TAB = "DELETE FROM " + KEY_DB_TAB;}
2.根据音乐播放生命周期的onAudioComplete中,进行数据库的保存:
@Overridepublic void onAudioComplete(IPlayer player, AudioInfo audio) {saveMusicRecord(audio);LogPlus.d("###onAudioComplete###");}...private static void saveMusicRecord(AudioInfo audio) {ThreadPoolExecutor.execute(() -> {SmartPlayerApplication.getMusicRecordOpenHelper().getWritableDatabase();String displayName = audio.displayName();SmartPlayerApplication.getMusicRecordOpenHelper().insertOrUpdateSong(displayName, "");});}
上面就实现了音乐播放文件名和播放次数的更新插入
3.在具体页面进行查询:
private List getDataFromDb() {List<MusicRankModel> list = new ArrayList<>();MusicRecordOpenHelper musicRecordOpenHelper = SmartPlayerApplication.getMusicRecordOpenHelper();musicRecordOpenHelper.getReadableDatabase();Cursor cursor = musicRecordOpenHelper.getMusicSortedByPlayCount();if (cursor != null && cursor.moveToFirst()) {do {@SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex(MusicRecordDao.COLUMN_NAME));@SuppressLint("Range") int playCount = cursor.getInt(cursor.getColumnIndex(MusicRecordDao.COLUMN_PLAY_COUNT));list.add(new MusicRankModel(name, playCount));} while (cursor.moveToNext());cursor.close();}if (list.size() > 100) {list = list.subList(0, 100);}return list;}
上面是经典的数据库写法,限制100个数量,
4.下面是UI层的展示
private void initData() {recyclerView = findViewById(R.id.rv_rank_list);recyclerView.setLayoutManager(new LinearLayoutManager(this));//从数据库获取musicList = getDataFromDb();//musicList = generateMusicList();adapter = new MusicRankAdapter(musicList);recyclerView.setAdapter(adapter);}
Adapter层做了TYPE_HEADER和TYPE_ITEM的处理:
public class MusicRankAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {private List<MusicRankModel> musicList;private static final int VIEW_TYPE_HEADER = 0;private static final int VIEW_TYPE_ITEM = 1;public static class MusicViewHolder extends RecyclerView.ViewHolder {public TextView musicName;public TextView playCount;public MusicViewHolder(@NonNull View itemView) {super(itemView);musicName = itemView.findViewById(R.id.musicName);playCount = itemView.findViewById(R.id.playCount);}}public static class HeaderViewHolder extends RecyclerView.ViewHolder {public HeaderViewHolder(@NonNull View itemView) {super(itemView);}}public MusicRankAdapter(List<MusicRankModel> musicList) {this.musicList = musicList;}@NonNull@Overridepublic RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {if (viewType == VIEW_TYPE_HEADER) {View headerView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_music_header, parent, false);return new HeaderViewHolder(headerView);} else {View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_music, parent, false);return new MusicViewHolder(itemView);}}@Overridepublic void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {if (holder instanceof MusicViewHolder) {MusicRankModel currentMusic = musicList.get(position - 1); // 减1因为第一个位置是头部视图((MusicViewHolder) holder).musicName.setText(currentMusic.getName());((MusicViewHolder) holder).playCount.setText(String.valueOf(currentMusic.getPlayCount()));}}@Overridepublic int getItemViewType(int position) {return position == 0 ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM;}@Overridepublic int getItemCount() {return musicList.size() + 1; // 加1表示包括头部视图}
}
具体item两个的layout:
item_music_header:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:padding="8dp"><TextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="2"android:text="歌曲名"android:gravity="center"android:textSize="16sp"android:textStyle="bold" /><TextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="播放次数"android:gravity="center"android:textSize="16sp"android:textStyle="bold" />
</LinearLayout>
item_music:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:padding="8dp"><TextViewandroid:id="@+id/musicName"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="2"android:gravity="center"android:text="Music Name"android:textSize="16sp" /><TextViewandroid:id="@+id/playCount"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Play Count"android:layout_weight="1"android:textSize="16sp"android:gravity="center"android:paddingStart="16dp"/>
</LinearLayout>
这样就实现了上面列表的效果。下面是时间戳记录和按月记录播放时长的思路放在下一篇去讲.
相关文章:
Android开发之音乐播放器添加排行需求
Music统计功能需求 1.记录歌曲名称与次数(歌曲播放结束算一次),根据播放次数制作一个排行列表;(开始说要记录歌手,后面debug发现这个字段没有,暂时不记录) 2.记录播放歌曲的时长,时间累加;&…...
latex 方括号编号
最近在做简历,需要列出发表的论文。 论文编号一般是采用[1]这种样式,但是找了几个简历模板里头没有直接包含这种编号样式。 我只好求助网络。 在CSDN上找了一圈,这篇博客给了一个思路:在\begin{enumerate}后面添加对应的样式即…...
Vue CLI 4与项目构建实战指南
title: Vue CLI 4与项目构建实战指南 date: 2024/6/9 updated: 2024/6/9 excerpt: 这篇文章介绍了如何使用Vue CLI优化项目构建配置,提高开发效率,涉及配置管理、项目部署策略、插件系统定制以及Webpack和TypeScript的深度集成技巧。 categories: 前端…...
深入解析Web通信 HTTP、HTTPS 和 WebSocket
在现代Web开发中,了解和掌握HTTP、HTTPS以及WebSocket协议是非常重要的。这些协议是实现Web应用程序之间通信的基石。本文将详细介绍这三种协议,包括它们的基本概念、工作原理、优缺点以及适用场景。通过深入解析它们的特点和应用,帮助读者更好地理解和使用这些协议。 一、…...
FISCO BCOS x GitLink,为国产开源技术生态注入新活力
作为中国领先的区块链底层平台之一,FISCO BCOS 自成立以来始终致力于推动国产开源区块链技术的应用和普及。近期,FISCO BCOS 将开源代码托管到CCF官方代码托管平台 GitLink (确实开源),为国产开源技术生态注入新活力。…...
Linux crontabs定时执行任务
文章目录 前言一、安装二、服务1. 启动crond服务2. 关闭crond服务3. 重启crond服务4. 设置crond开机启动5. 禁用crond开机启动6. 查看crond是否开机启动7. 重新载入配置8. 查看crond运行状态 三、使用1. 查看当前用户的crontab2. 编辑用户的crontab3. 删除用户的crontab的内容 …...
QNX简述
文章目录 前言1. QNX简介1.1 什么是QNX1.2 QNX的应用场景1.3 QNX的优点1.4 QNX的发展史1.5 QNX的商业模式 2. QNX的技术特点3. QNX和其它操作系统的比较3.1 QNX VS LINUX3.2 QNX VS FreeRTOS3.3 QNX VS 鸿蒙操作系统 4. 我的疑问4.1 微内核看起来又稳定又容易调试,为…...
[Llama3] ReAct Prompt 测试实验
ReAct 是一种 LLM 提示和结果处理方法,结合了推理、行动计划和知识源整合,使 LLM 超越其语言模型,并在预测中使用来自现实世界的信息。 ReAct 是推理和行动的结合。 介绍 ReAct 的论文表明它比思维链提示更好。与后者不同的是,Re…...
nodejs 某音douyin网页端搜索接口及x_bogus、a_bogus(包含完整源码)(2024-06-13)
前言 x_bogus或a_bogus算法大概是对数据、ua、时间戳、浏览器的几个指纹进行计算,拿到一个110位大数组,然后转字符,在头部再添加十二位随机字符,再进行魔改的base64加密。 问:抖音的x_bogus、a_bogus值有什么用&#x…...
继承深度剖析
前言 从继承开始就开始C进阶了, 这一块需要好好学习,这块知识很重要, 坑有点多,所以是面试笔试的常客。 基本概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段, 它允许程序员在保持原有…...
使用 Vue 和 Ant Design 实现抽屉效果的模块折叠功能
功能描述: 有两个模块,点击上面模块的收起按钮时,上面的模块可以折叠,下面的模块随之扩展 代码实现: 我们在 Vue 组件中定义两个模块的布局和状态管理: const scrollTableY ref(560); // 表格初始高度…...
Springboot整合SpringCache+redis简化缓存开发
使用步骤: 1.引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId> </dependency><dependency><groupId>org.springframework.boot</groupI…...
关于EOF标识符
EOF的概念 EOF是C语言中表示文件结束的标志符号,通常被定义为-1,它用于指示已到达文件的末尾或输入流的末尾。 EOF的使用 在输入操作中,EOF常常用于判断是否到达了文件末尾或输入流末尾,以便终止读取操作。例如,在使…...
家用洗地机排行榜前十名:2024十大王牌机型精准种草
最近很多人都在问我洗地机相关的问题,不愧是改善家庭生活品质的“三神器”之一。洗地机依靠其清洁力和清洁效率吸引了越来越多的平时需要做家务人群的兴趣,为了解答大家关于洗地机的各种疑问,我把市面上目前非常火爆的洗地机型号和参数都进行…...
【Chrome插件】如何在Chrome插件开发中处理复杂数据结构的存储
最近俺在接触 Chrome 插件开发,需要把一个数据存放到浏览器的存储中。这个数据结构有点复杂,它包含一个 Map 和一个数组。我使用 chrome.storage.local API来存储这个数据,然后在另一个地方获取数据。保存数据的代码并没有报错,但…...
MySQL 保姆级教程(二):使用 MySQL 检索数据
使用 MySQL 3.2 选择数据库 使用数据库: 输入: USE 数据库名;输出: Database changed分析: 不返回任何结果,显示某种形式的通知 例如: 使用 crashcourse 数据库 use crashcourse; 3.3 了解数据库和表 列出所有的数据库: 输入: SHOW DATABASES;输出: --------…...
Sui Bridge在测试网上线并推出10万SUI激励计划
是一种为Sui设计的原生桥接协议,专门用于在Sui与其他网络之间桥接资产和数据。今天,Sui Bridge宣布在测试网上线。作为一种原生协议,Sui Bridge能够在Ethereum和Sui之间轻松且安全地转移ETH、wBTC、USDC和USDT,使其成为Sui基础设施…...
Spring系统学习 - Bean的作用域
bean作用域介绍 Spring框架提供了不同的作用域来管理Bean的生命周期和可见性,这对于控制不同类型的组件和处理并发请求尤其重要。 singleton(默认): 每个Spring IoC容器只有一个bean实例。当容器创建bean后,它会被缓存…...
贪吃蛇双人模式设计(2)
敲上瘾-CSDN博客控制台程序设置_c语言控制程序窗口大小-CSDN博客贪吃蛇小游戏_贪吃蛇小游戏csdn-CSDN博客 一、功能实现: 玩家1使用↓ → ← ↑按键来操作蛇的方向,使用右Shift键加速,右Ctrl键减速玩家2使用W A S D按键来操作蛇的方向&am…...
mysql什么时候不需要建立索引
WHERE 条件,GROUP BY,ORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。字段中存在大量重复数据,不需要创建索引,比如性…...
wordpress后台更新后 前端没变化的解决方法
使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…...
利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...
3.3.1_1 检错编码(奇偶校验码)
从这节课开始,我们会探讨数据链路层的差错控制功能,差错控制功能的主要目标是要发现并且解决一个帧内部的位错误,我们需要使用特殊的编码技术去发现帧内部的位错误,当我们发现位错误之后,通常来说有两种解决方案。第一…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...
苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...
深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南
🚀 C extern 关键字深度解析:跨文件编程的终极指南 📅 更新时间:2025年6月5日 🏷️ 标签:C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言🔥一、extern 是什么?&…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
处理vxe-table 表尾数据是单独一个接口,表格tableData数据更新后,需要点击两下,表尾才是正确的
修改bug思路: 分别把 tabledata 和 表尾相关数据 console.log() 发现 更新数据先后顺序不对 settimeout延迟查询表格接口 ——测试可行 升级↑:async await 等接口返回后再开始下一个接口查询 ________________________________________________________…...
【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...
