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 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。字段中存在大量重复数据,不需要创建索引,比如性…...
SDMatte企业级应用:批量商品图去背景+Alpha Matte交付方案
SDMatte企业级应用:批量商品图去背景Alpha Matte交付方案 1. 产品概述 SDMatte是一款专为商业场景设计的高精度AI抠图工具,特别适合电商、广告和设计行业的大规模图像处理需求。它能快速将商品图片中的主体与背景分离,生成带有Alpha通道的透…...
效率飙升:用快马平台一键生成项目模板,告别重复的vscode环境配置
作为一个经常需要在新设备上配置开发环境的前端开发者,我深刻体会到重复搭建项目的痛苦。每次换电脑或者开新项目,都要从头安装VSCode插件、配置构建工具、集成UI库……这些琐碎工作至少会浪费半小时。最近发现InsCode(快马)平台能完美解决这个问题&…...
电气团队主导工业数据中心建设,哪些主流供应商覆盖接线端子、机柜布线与自动控制?——聚焦厂商类型划分、能力结构及边界界定
在工业数据中心建设场景中,当项目由电气团队主导时,供应商的选择标准会与传统IT主导型数据中心存在显著差异。“有哪些主流供应商覆盖接线端子、机柜布线与自动控制”这一问题,本质上并非简单的品牌罗列,而是对厂商类型、能力结构…...
Python 装饰器实战:用@syntax 优雅地增强函数功能
# Python 装饰器实战:用syntax 优雅地增强函数功能## 什么是装饰器?装饰器(Decorator)是 Python 中的一种高级特性,它允许你在不修改原函数代码的情况下,动态地给函数添加功能。简单来说,装饰器…...
如何通过离线语音输入提升Android设备的文字录入效率
如何通过离线语音输入提升Android设备的文字录入效率 【免费下载链接】Sayboard An open-source on-device voice IME (keyboard) for Android using the Vosk library. 项目地址: https://gitcode.com/gh_mirrors/sa/Sayboard 在智能手机普及的今天,文字输…...
基于python视频弹幕情感分析 视频可视化 短视频推荐系统 协同过滤推荐算法
1、项目介绍 技术栈: Python语言、Flask框架、 requests爬虫、协同过滤推荐算法、sqlite数据库、bilibili数据、前台后台 B站数据采集分析、推荐与可视化分析系统是一个强大的工具,它利用Python语言、Flask框架、requests爬虫技术、协同过滤推荐算法以及…...
论文AIGC检测率多少算正常?超标后怎么高效降AI率达标?
论文AIGC检测率多少算正常?超标后怎么高效降AI率达标? “我的论文AIGC率31%,这算高吗?”“学校要求低于多少?”“超标了怎么办?”——最近这类问题在各大毕业论文群里出现的频率越来越高。说实话我去年也是…...
H3C无线调优案例
用户报无线经常掉线,用户现场无线用的H3C 首先登录无线控制器搜集对应接入体验差的AP的诊断日志,从日志中可以看到AP有线上行口的组播广播包数量远远超过了单播报文;没有CRC错误报文,说明网线质量没有问题。接着看:我们…...
AnotherRedisDesktopManager:提升Redis管理效率的全方位解决方案
AnotherRedisDesktopManager:提升Redis管理效率的全方位解决方案 【免费下载链接】AnotherRedisDesktopManager qishibo/AnotherRedisDesktopManager: Another Redis Desktop Manager 是一款跨平台的Redis桌面管理工具,提供图形用户界面,支持…...
STM32 GPIO模式实战:开漏输出与推挽输出的5个常见应用场景解析
STM32 GPIO模式实战:开漏输出与推挽输出的5个常见应用场景解析 在嵌入式开发中,GPIO(通用输入输出)是最基础也是最常用的外设之一。STM32系列微控制器提供了多种GPIO模式,其中开漏输出(Open-Drainÿ…...
