commit 04c1a63caa523e78fe00a14cc11b7b967a530c9f
Author: muqing <1966944300@qq.com>
Date: Mon Jul 22 20:54:14 2024 +0800
git fetch origin
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..70cf89a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+*.apk
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dictionaries/19669.xml b/.idea/dictionaries/19669.xml
new file mode 100644
index 0000000..6dac566
--- /dev/null
+++ b/.idea/dictionaries/19669.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..d170b6e
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..2823ce9
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..1b261c7
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..2b8a50f
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..932c4df
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ab0f144
--- /dev/null
+++ b/README.md
@@ -0,0 +1,47 @@
+## 云音乐是什么?
+一个对接网易云的音乐播放器
+* 内置集成作者自己写的歌词Lrc组件支持单行歌词和多行歌词支持悬浮窗歌词。
+* 内置适配Android13的通知栏 (不完善请大佬请教)
+* 对接了网易云的歌单,歌曲,搜索,二维码登录等其余功能。
+* main.java-内api变量是网易云SDK后台服务器地址(可变更)
+* http 是我服务器php接口的地址用于软件更新用或者其他功能(个根据需要做出必须的更改或者删除)
+
+## 进度
+* UI
+ * 歌单
+ * 音乐播放选择列表
+ * 音乐播放器控制界面
+ * 登陆
+ * 设置
+ * 侧滑栏内小功能
+* 代码
+ * 适配主题和UI美化
+ * 维护接口(不完善)
+ * 蓝牙功能
+ * 通知栏控制
+ * 悬浮歌词功能
+ * 基本播放器功能(控制暂停 上下曲 播放歌单操作)
+
+
+## 截图
+
+
+
+
+## 在使用中有任何问题,欢迎反馈给我,可以用以下联系方式跟我交流
+ * QQ:1966944300
+
+## 后台
+ * Github: [网易云音乐 API](https://github.com/Binaryify/NeteaseCloudMusicApi)
+
+## 关于
+在兴趣的驱动下,写一个`免费`的东西,有欣喜,也还有汗水,希望你喜欢我的作品,同时也能支持一下。
+
+
+## 修改JAR的包
+```javascript
+[//]: # (主要修改内容MD3化)
+com.github.QuadFlask:colorpicker:0.0.15
+[//]: # (歌词做全局变量给悬浮窗歌词)
+'com.github.wangchenyan:lrcview:2.2.1'
+...
\ No newline at end of file
diff --git a/StatusBarUtil/.gitignore b/StatusBarUtil/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/StatusBarUtil/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/StatusBarUtil/build.gradle b/StatusBarUtil/build.gradle
new file mode 100644
index 0000000..a40a6c3
--- /dev/null
+++ b/StatusBarUtil/build.gradle
@@ -0,0 +1,25 @@
+apply plugin: 'com.android.library'
+version = "1.5.1"
+android {
+ namespace 'com.jaeger.library'
+ compileSdk 34
+ resourcePrefix "statusbarutil_"
+ defaultConfig {
+ minSdkVersion 23
+ //noinspection ExpiredTargetSdkVersion
+ targetSdk 31
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.9.0'
+}
\ No newline at end of file
diff --git a/StatusBarUtil/src/main/AndroidManifest.xml b/StatusBarUtil/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..758b0af
--- /dev/null
+++ b/StatusBarUtil/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/StatusBarUtil/src/main/java/com/jaeger/library/StatusBarUtil.java b/StatusBarUtil/src/main/java/com/jaeger/library/StatusBarUtil.java
new file mode 100644
index 0000000..aa0fe40
--- /dev/null
+++ b/StatusBarUtil/src/main/java/com/jaeger/library/StatusBarUtil.java
@@ -0,0 +1,726 @@
+package com.jaeger.library;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.drawerlayout.widget.DrawerLayout;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ * Created by Jaeger on 16/2/14.
+ *
+ * Email: chjie.jaeger@gmail.com
+ * GitHub: ...
+ */
+public class StatusBarUtil {
+
+ public static final int DEFAULT_STATUS_BAR_ALPHA = 112;
+ private static final int FAKE_STATUS_BAR_VIEW_ID = R.id.statusbarutil_fake_status_bar_view;
+ private static final int FAKE_TRANSLUCENT_VIEW_ID = R.id.statusbarutil_translucent_view;
+ private static final int TAG_KEY_HAVE_SET_OFFSET = -123;
+
+ /**
+ * 设置状态栏颜色
+ *
+ * @param activity 需要设置的 activity
+ * @param color 状态栏颜色值
+ */
+ public static void setColor(Activity activity, @ColorInt int color) {
+ setColor(activity, color, DEFAULT_STATUS_BAR_ALPHA);
+ }
+
+ /**
+ * 设置状态栏颜色
+ *
+ * @param activity 需要设置的activity
+ * @param color 状态栏颜色值
+ * @param statusBarAlpha 状态栏透明度
+ */
+
+ public static void setColor(Activity activity, @ColorInt int color, @IntRange(from = 0, to = 255) int statusBarAlpha) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ activity.getWindow().setStatusBarColor(calculateStatusColor(color, statusBarAlpha));
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
+ View fakeStatusBarView = decorView.findViewById(FAKE_STATUS_BAR_VIEW_ID);
+ if (fakeStatusBarView != null) {
+ if (fakeStatusBarView.getVisibility() == View.GONE) {
+ fakeStatusBarView.setVisibility(View.VISIBLE);
+ }
+ fakeStatusBarView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha));
+ } else {
+ decorView.addView(createStatusBarView(activity, color, statusBarAlpha));
+ }
+ setRootView(activity);
+ }
+ }
+
+ /**
+ * 为滑动返回界面设置状态栏颜色
+ *
+ * @param activity 需要设置的activity
+ * @param color 状态栏颜色值
+ */
+ public static void setColorForSwipeBack(Activity activity, int color) {
+ setColorForSwipeBack(activity, color, DEFAULT_STATUS_BAR_ALPHA);
+ }
+
+ /**
+ * 为滑动返回界面设置状态栏颜色
+ *
+ * @param activity 需要设置的activity
+ * @param color 状态栏颜色值
+ * @param statusBarAlpha 状态栏透明度
+ */
+ public static void setColorForSwipeBack(Activity activity, @ColorInt int color,
+ @IntRange(from = 0, to = 255) int statusBarAlpha) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+
+ ViewGroup contentView = ((ViewGroup) activity.findViewById(android.R.id.content));
+ View rootView = contentView.getChildAt(0);
+ int statusBarHeight = getStatusBarHeight(activity);
+ if (rootView instanceof CoordinatorLayout) {
+ final CoordinatorLayout coordinatorLayout = (CoordinatorLayout) rootView;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ coordinatorLayout.setFitsSystemWindows(false);
+ contentView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha));
+ boolean isNeedRequestLayout = contentView.getPaddingTop() < statusBarHeight;
+ if (isNeedRequestLayout) {
+ contentView.setPadding(0, statusBarHeight, 0, 0);
+ coordinatorLayout.post(coordinatorLayout::requestLayout);
+ }
+ } else {
+ coordinatorLayout.setStatusBarBackgroundColor(calculateStatusColor(color, statusBarAlpha));
+ }
+ } else {
+ contentView.setPadding(0, statusBarHeight, 0, 0);
+ contentView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha));
+ }
+ setTransparentForWindow(activity);
+ }
+ }
+
+ /**
+ * 设置状态栏纯色 不加半透明效果
+ *
+ * @param activity 需要设置的 activity
+ * @param color 状态栏颜色值
+ */
+ public static void setColorNoTranslucent(Activity activity, @ColorInt int color) {
+ setColor(activity, color, 0);
+ }
+
+ /**
+ * 设置状态栏颜色(5.0以下无半透明效果,不建议使用)
+ *
+ * @param activity 需要设置的 activity
+ * @param color 状态栏颜色值
+ */
+ @Deprecated
+ public static void setColorDiff(Activity activity, @ColorInt int color) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ return;
+ }
+ transparentStatusBar(activity);
+ ViewGroup contentView = (ViewGroup) activity.findViewById(android.R.id.content);
+ // 移除半透明矩形,以免叠加
+ View fakeStatusBarView = contentView.findViewById(FAKE_STATUS_BAR_VIEW_ID);
+ if (fakeStatusBarView != null) {
+ if (fakeStatusBarView.getVisibility() == View.GONE) {
+ fakeStatusBarView.setVisibility(View.VISIBLE);
+ }
+ fakeStatusBarView.setBackgroundColor(color);
+ } else {
+ contentView.addView(createStatusBarView(activity, color));
+ }
+ setRootView(activity);
+ }
+
+ /**
+ * 使状态栏半透明
+ *
+ * 适用于图片作为背景的界面,此时需要图片填充到状态栏
+ *
+ * @param activity 需要设置的activity
+ */
+ public static void setTranslucent(Activity activity) {
+ setTranslucent(activity, DEFAULT_STATUS_BAR_ALPHA);
+ }
+
+ /**
+ * 使状态栏半透明
+ *
+ * 适用于图片作为背景的界面,此时需要图片填充到状态栏
+ *
+ * @param activity 需要设置的activity
+ * @param statusBarAlpha 状态栏透明度
+ */
+ public static void setTranslucent(Activity activity, @IntRange(from = 0, to = 255) int statusBarAlpha) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ return;
+ }
+ setTransparent(activity);
+ addTranslucentView(activity, statusBarAlpha);
+ }
+
+ /**
+ * 针对根布局是 CoordinatorLayout, 使状态栏半透明
+ *
+ * 适用于图片作为背景的界面,此时需要图片填充到状态栏
+ *
+ * @param activity 需要设置的activity
+ * @param statusBarAlpha 状态栏透明度
+ */
+ public static void setTranslucentForCoordinatorLayout(Activity activity, @IntRange(from = 0, to = 255) int statusBarAlpha) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ return;
+ }
+ transparentStatusBar(activity);
+ addTranslucentView(activity, statusBarAlpha);
+ }
+
+ /**
+ * 设置状态栏全透明
+ *
+ * @param activity 需要设置的activity
+ */
+ public static void setTransparent(Activity activity) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ return;
+ }
+ transparentStatusBar(activity);
+ setRootView(activity);
+ }
+
+ /**
+ * 使状态栏透明(5.0以上半透明效果,不建议使用)
+ *
+ * 适用于图片作为背景的界面,此时需要图片填充到状态栏
+ *
+ * @param activity 需要设置的activity
+ */
+ @Deprecated
+ public static void setTranslucentDiff(Activity activity) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ // 设置状态栏透明
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ setRootView(activity);
+ }
+ }
+
+ /**
+ * 为DrawerLayout 布局设置状态栏变色
+ *
+ * @param activity 需要设置的activity
+ * @param drawerLayout DrawerLayout
+ * @param color 状态栏颜色值
+ */
+ public static void setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, @ColorInt int color) {
+ setColorForDrawerLayout(activity, drawerLayout, color, DEFAULT_STATUS_BAR_ALPHA);
+ }
+
+ /**
+ * 为DrawerLayout 布局设置状态栏颜色,纯色
+ *
+ * @param activity 需要设置的activity
+ * @param drawerLayout DrawerLayout
+ * @param color 状态栏颜色值
+ */
+ public static void setColorNoTranslucentForDrawerLayout(Activity activity, DrawerLayout drawerLayout, @ColorInt int color) {
+ setColorForDrawerLayout(activity, drawerLayout, color, 0);
+ }
+
+ /**
+ * 为DrawerLayout 布局设置状态栏变色
+ *
+ * @param activity 需要设置的activity
+ * @param drawerLayout DrawerLayout
+ * @param color 状态栏颜色值
+ * @param statusBarAlpha 状态栏透明度
+ */
+ public static void setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, @ColorInt int color,
+ @IntRange(from = 0, to = 255) int statusBarAlpha) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ return;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
+ } else {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+ // 生成一个状态栏大小的矩形
+ // 添加 statusBarView 到布局中
+ ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0);
+ View fakeStatusBarView = contentLayout.findViewById(FAKE_STATUS_BAR_VIEW_ID);
+ if (fakeStatusBarView != null) {
+ if (fakeStatusBarView.getVisibility() == View.GONE) {
+ fakeStatusBarView.setVisibility(View.VISIBLE);
+ }
+ fakeStatusBarView.setBackgroundColor(color);
+ } else {
+ contentLayout.addView(createStatusBarView(activity, color), 0);
+ }
+ // 内容布局不是 LinearLayout 时,设置padding top
+ if (!(contentLayout instanceof LinearLayout) && contentLayout.getChildAt(1) != null) {
+ contentLayout.getChildAt(1)
+ .setPadding(contentLayout.getPaddingLeft(), getStatusBarHeight(activity) + contentLayout.getPaddingTop(),
+ contentLayout.getPaddingRight(), contentLayout.getPaddingBottom());
+ }
+ // 设置属性
+ setDrawerLayoutProperty(drawerLayout, contentLayout);
+ addTranslucentView(activity, statusBarAlpha);
+ }
+
+ /**
+ * 设置 DrawerLayout 属性
+ *
+ * @param drawerLayout DrawerLayout
+ * @param drawerLayoutContentLayout DrawerLayout 的内容布局
+ */
+ private static void setDrawerLayoutProperty(DrawerLayout drawerLayout, ViewGroup drawerLayoutContentLayout) {
+ ViewGroup drawer = (ViewGroup) drawerLayout.getChildAt(1);
+ drawerLayout.setFitsSystemWindows(false);
+ drawerLayoutContentLayout.setFitsSystemWindows(false);
+ drawerLayoutContentLayout.setClipToPadding(true);
+ drawer.setFitsSystemWindows(false);
+ }
+
+ /**
+ * 为DrawerLayout 布局设置状态栏变色(5.0以下无半透明效果,不建议使用)
+ *
+ * @param activity 需要设置的activity
+ * @param drawerLayout DrawerLayout
+ * @param color 状态栏颜色值
+ */
+ @Deprecated
+ public static void setColorForDrawerLayoutDiff(Activity activity, DrawerLayout drawerLayout, @ColorInt int color) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ // 生成一个状态栏大小的矩形
+ ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0);
+ View fakeStatusBarView = contentLayout.findViewById(FAKE_STATUS_BAR_VIEW_ID);
+ if (fakeStatusBarView != null) {
+ if (fakeStatusBarView.getVisibility() == View.GONE) {
+ fakeStatusBarView.setVisibility(View.VISIBLE);
+ }
+ fakeStatusBarView.setBackgroundColor(calculateStatusColor(color, DEFAULT_STATUS_BAR_ALPHA));
+ } else {
+ // 添加 statusBarView 到布局中
+ contentLayout.addView(createStatusBarView(activity, color), 0);
+ }
+ // 内容布局不是 LinearLayout 时,设置padding top
+ if (!(contentLayout instanceof LinearLayout) && contentLayout.getChildAt(1) != null) {
+ contentLayout.getChildAt(1).setPadding(0, getStatusBarHeight(activity), 0, 0);
+ }
+ // 设置属性
+ setDrawerLayoutProperty(drawerLayout, contentLayout);
+ }
+ }
+
+ /**
+ * 为 DrawerLayout 布局设置状态栏透明
+ *
+ * @param activity 需要设置的activity
+ * @param drawerLayout DrawerLayout
+ */
+ public static void setTranslucentForDrawerLayout(Activity activity, DrawerLayout drawerLayout) {
+ setTranslucentForDrawerLayout(activity, drawerLayout, DEFAULT_STATUS_BAR_ALPHA);
+ }
+
+ /**
+ * 为 DrawerLayout 布局设置状态栏透明
+ *
+ * @param activity 需要设置的activity
+ * @param drawerLayout DrawerLayout
+ */
+ public static void setTranslucentForDrawerLayout(Activity activity, DrawerLayout drawerLayout,
+ @IntRange(from = 0, to = 255) int statusBarAlpha) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ return;
+ }
+ setTransparentForDrawerLayout(activity, drawerLayout);
+ addTranslucentView(activity, statusBarAlpha);
+ }
+
+ /**
+ * 为 DrawerLayout 布局设置状态栏透明
+ *
+ * @param activity 需要设置的activity
+ * @param drawerLayout DrawerLayout
+ */
+ public static void setTransparentForDrawerLayout(Activity activity, DrawerLayout drawerLayout) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ return;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
+ } else {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+
+ ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0);
+ // 内容布局不是 LinearLayout 时,设置padding top
+ if (!(contentLayout instanceof LinearLayout) && contentLayout.getChildAt(1) != null) {
+ contentLayout.getChildAt(1).setPadding(0, getStatusBarHeight(activity), 0, 0);
+ }
+
+ // 设置属性
+ setDrawerLayoutProperty(drawerLayout, contentLayout);
+ }
+
+ /**
+ * 为 DrawerLayout 布局设置状态栏透明(5.0以上半透明效果,不建议使用)
+ *
+ * @param activity 需要设置的activity
+ * @param drawerLayout DrawerLayout
+ */
+ @Deprecated
+ public static void setTranslucentForDrawerLayoutDiff(Activity activity, DrawerLayout drawerLayout) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ // 设置状态栏透明
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ // 设置内容布局属性
+ ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0);
+ contentLayout.setFitsSystemWindows(true);
+ contentLayout.setClipToPadding(true);
+ // 设置抽屉布局属性
+ ViewGroup vg = (ViewGroup) drawerLayout.getChildAt(1);
+ vg.setFitsSystemWindows(false);
+ // 设置 DrawerLayout 属性
+ drawerLayout.setFitsSystemWindows(false);
+ }
+ }
+
+ /**
+ * 为头部是 ImageView 的界面设置状态栏全透明
+ *
+ * @param activity 需要设置的activity
+ * @param needOffsetView 需要向下偏移的 View
+ */
+ public static void setTransparentForImageView(Activity activity, View needOffsetView) {
+ setTranslucentForImageView(activity, 0, needOffsetView);
+ }
+
+ /**
+ * 为头部是 ImageView 的界面设置状态栏透明(使用默认透明度)
+ *
+ * @param activity 需要设置的activity
+ * @param needOffsetView 需要向下偏移的 View
+ */
+ public static void setTranslucentForImageView(Activity activity, View needOffsetView) {
+ setTranslucentForImageView(activity, DEFAULT_STATUS_BAR_ALPHA, needOffsetView);
+ }
+
+ /**
+ * 为头部是 ImageView 的界面设置状态栏透明
+ *
+ * @param activity 需要设置的activity
+ * @param statusBarAlpha 状态栏透明度
+ * @param needOffsetView 需要向下偏移的 View
+ */
+ public static void setTranslucentForImageView(Activity activity, @IntRange(from = 0, to = 255) int statusBarAlpha,
+ View needOffsetView) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ return;
+ }
+ setTransparentForWindow(activity);
+ addTranslucentView(activity, statusBarAlpha);
+ if (needOffsetView != null) {
+ Object haveSetOffset = needOffsetView.getTag(TAG_KEY_HAVE_SET_OFFSET);
+ if (haveSetOffset != null && (Boolean) haveSetOffset) {
+ return;
+ }
+ ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) needOffsetView.getLayoutParams();
+ layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin + getStatusBarHeight(activity),
+ layoutParams.rightMargin, layoutParams.bottomMargin);
+ needOffsetView.setTag(TAG_KEY_HAVE_SET_OFFSET, true);
+ }
+ }
+
+ /**
+ * 为 fragment 头部是 ImageView 的设置状态栏透明
+ *
+ * @param activity fragment 对应的 activity
+ * @param needOffsetView 需要向下偏移的 View
+ */
+ public static void setTranslucentForImageViewInFragment(Activity activity, View needOffsetView) {
+ setTranslucentForImageViewInFragment(activity, DEFAULT_STATUS_BAR_ALPHA, needOffsetView);
+ }
+
+ /**
+ * 为 fragment 头部是 ImageView 的设置状态栏透明
+ *
+ * @param activity fragment 对应的 activity
+ * @param needOffsetView 需要向下偏移的 View
+ */
+ public static void setTransparentForImageViewInFragment(Activity activity, View needOffsetView) {
+ setTranslucentForImageViewInFragment(activity, 0, needOffsetView);
+ }
+
+ /**
+ * 为 fragment 头部是 ImageView 的设置状态栏透明
+ *
+ * @param activity fragment 对应的 activity
+ * @param statusBarAlpha 状态栏透明度
+ * @param needOffsetView 需要向下偏移的 View
+ */
+ public static void setTranslucentForImageViewInFragment(Activity activity, @IntRange(from = 0, to = 255) int statusBarAlpha,
+ View needOffsetView) {
+ setTranslucentForImageView(activity, statusBarAlpha, needOffsetView);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ clearPreviousSetting(activity);
+ }
+ }
+
+ /**
+ * 隐藏伪状态栏 View
+ *
+ * @param activity 调用的 Activity
+ */
+ public static void hideFakeStatusBarView(Activity activity) {
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
+ View fakeStatusBarView = decorView.findViewById(FAKE_STATUS_BAR_VIEW_ID);
+ if (fakeStatusBarView != null) {
+ fakeStatusBarView.setVisibility(View.GONE);
+ }
+ View fakeTranslucentView = decorView.findViewById(FAKE_TRANSLUCENT_VIEW_ID);
+ if (fakeTranslucentView != null) {
+ fakeTranslucentView.setVisibility(View.GONE);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ public static void setLightMode(Activity activity) {
+ setMIUIStatusBarDarkIcon(activity, true);
+ setMeizuStatusBarDarkIcon(activity, true);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ public static void setDarkMode(Activity activity) {
+ setMIUIStatusBarDarkIcon(activity, false);
+ setMeizuStatusBarDarkIcon(activity, false);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ }
+ }
+
+ /**
+ * 修改 MIUI V6 以上状态栏颜色
+ */
+ private static void setMIUIStatusBarDarkIcon(@NonNull Activity activity, boolean darkIcon) {
+ Class extends Window> clazz = activity.getWindow().getClass();
+ try {
+ Class> layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
+ Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
+ int darkModeFlag = field.getInt(layoutParams);
+ Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
+ extraFlagField.invoke(activity.getWindow(), darkIcon ? darkModeFlag : 0, darkModeFlag);
+ } catch (Exception e) {
+ //e.printStackTrace();
+ }
+ }
+
+ /**
+ * 修改魅族状态栏字体颜色 Flyme 4.0
+ */
+ private static void setMeizuStatusBarDarkIcon(@NonNull Activity activity, boolean darkIcon) {
+ try {
+ WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
+ Field darkFlag = WindowManager.LayoutParams.class.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
+ Field meizuFlags = WindowManager.LayoutParams.class.getDeclaredField("meizuFlags");
+ darkFlag.setAccessible(true);
+ meizuFlags.setAccessible(true);
+ int bit = darkFlag.getInt(null);
+ int value = meizuFlags.getInt(lp);
+ if (darkIcon) {
+ value |= bit;
+ } else {
+ value &= ~bit;
+ }
+ meizuFlags.setInt(lp, value);
+ activity.getWindow().setAttributes(lp);
+ } catch (Exception e) {
+ //e.printStackTrace();
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private static void clearPreviousSetting(Activity activity) {
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
+ View fakeStatusBarView = decorView.findViewById(FAKE_STATUS_BAR_VIEW_ID);
+ if (fakeStatusBarView != null) {
+ decorView.removeView(fakeStatusBarView);
+ ViewGroup rootView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);
+ rootView.setPadding(0, 0, 0, 0);
+ }
+ }
+
+ /**
+ * 添加半透明矩形条
+ *
+ * @param activity 需要设置的 activity
+ * @param statusBarAlpha 透明值
+ */
+ private static void addTranslucentView(Activity activity, @IntRange(from = 0, to = 255) int statusBarAlpha) {
+ ViewGroup contentView = (ViewGroup) activity.findViewById(android.R.id.content);
+ View fakeTranslucentView = contentView.findViewById(FAKE_TRANSLUCENT_VIEW_ID);
+ if (fakeTranslucentView != null) {
+ if (fakeTranslucentView.getVisibility() == View.GONE) {
+ fakeTranslucentView.setVisibility(View.VISIBLE);
+ }
+ fakeTranslucentView.setBackgroundColor(Color.argb(statusBarAlpha, 0, 0, 0));
+ } else {
+ contentView.addView(createTranslucentStatusBarView(activity, statusBarAlpha));
+ }
+ }
+
+ /**
+ * 生成一个和状态栏大小相同的彩色矩形条
+ *
+ * @param activity 需要设置的 activity
+ * @param color 状态栏颜色值
+ * @return 状态栏矩形条
+ */
+ private static View createStatusBarView(Activity activity, @ColorInt int color) {
+ return createStatusBarView(activity, color, 0);
+ }
+
+ /**
+ * 生成一个和状态栏大小相同的半透明矩形条
+ *
+ * @param activity 需要设置的activity
+ * @param color 状态栏颜色值
+ * @param alpha 透明值
+ * @return 状态栏矩形条
+ */
+ private static View createStatusBarView(Activity activity, @ColorInt int color, int alpha) {
+ // 绘制一个和状态栏一样高的矩形
+ View statusBarView = new View(activity);
+ LinearLayout.LayoutParams params =
+ new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity));
+ statusBarView.setLayoutParams(params);
+ statusBarView.setBackgroundColor(calculateStatusColor(color, alpha));
+ statusBarView.setId(FAKE_STATUS_BAR_VIEW_ID);
+ return statusBarView;
+ }
+
+ /**
+ * 设置根布局参数
+ */
+ private static void setRootView(Activity activity) {
+ ViewGroup parent = (ViewGroup) activity.findViewById(android.R.id.content);
+ for (int i = 0, count = parent.getChildCount(); i < count; i++) {
+ View childView = parent.getChildAt(i);
+ if (childView instanceof ViewGroup) {
+ childView.setFitsSystemWindows(true);
+ ((ViewGroup) childView).setClipToPadding(true);
+ }
+ }
+ }
+
+ /**
+ * 设置透明
+ */
+ private static void setTransparentForWindow(Activity activity) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
+ activity.getWindow()
+ .getDecorView()
+ .setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ activity.getWindow()
+ .setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+ }
+
+ /**
+ * 使状态栏透明
+ */
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private static void transparentStatusBar(Activity activity) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+ activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
+ } else {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+ }
+
+ /**
+ * 创建半透明矩形 View
+ *
+ * @param alpha 透明值
+ * @return 半透明 View
+ */
+ private static View createTranslucentStatusBarView(Activity activity, int alpha) {
+ // 绘制一个和状态栏一样高的矩形
+ View statusBarView = new View(activity);
+ LinearLayout.LayoutParams params =
+ new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity));
+ statusBarView.setLayoutParams(params);
+ statusBarView.setBackgroundColor(Color.argb(alpha, 0, 0, 0));
+ statusBarView.setId(FAKE_TRANSLUCENT_VIEW_ID);
+ return statusBarView;
+ }
+
+ /**
+ * 获取状态栏高度
+ *
+ * @param context context
+ * @return 状态栏高度
+ */
+ private static int getStatusBarHeight(Context context) {
+ // 获得状态栏高度
+ int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
+ return context.getResources().getDimensionPixelSize(resourceId);
+ }
+
+ /**
+ * 计算状态栏颜色
+ *
+ * @param color color值
+ * @param alpha alpha值
+ * @return 最终的状态栏颜色
+ */
+ private static int calculateStatusColor(@ColorInt int color, int alpha) {
+ if (alpha == 0) {
+ return color;
+ }
+ float a = 1 - alpha / 255f;
+ int red = color >> 16 & 0xff;
+ int green = color >> 8 & 0xff;
+ int blue = color & 0xff;
+ red = (int) (red * a + 0.5);
+ green = (int) (green * a + 0.5);
+ blue = (int) (blue * a + 0.5);
+ return 0xff << 24 | red << 16 | green << 8 | blue;
+ }
+}
diff --git a/StatusBarUtil/src/main/res/values/ids.xml b/StatusBarUtil/src/main/res/values/ids.xml
new file mode 100644
index 0000000..faad20c
--- /dev/null
+++ b/StatusBarUtil/src/main/res/values/ids.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..67b19d6
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,78 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+android {
+ signingConfigs {
+ debug {
+ storeFile file('../muqing.jks')
+ storePassword 'muqing153'
+ keyAlias 'muqing'
+ keyPassword 'muqing153'
+ }
+ }
+ namespace 'com.muqingbfq'
+ compileSdk 33
+ defaultConfig {
+ applicationId "com.muqingbfq"
+ minSdk 23
+ //noinspection ExpiredTargetSdkVersion,OldTargetApi
+ targetSdk 33
+ versionCode 1
+ versionName "2.6.0"
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ buildTypes {
+ release {
+// shrinkResources true
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ android.applicationVariants.configureEach {
+ variant ->
+ variant.outputs.configureEach {
+ //在这里修改apk文件名
+ outputFileName = "Cloud_music-${variant.name}-v${variant.versionName}.apk"
+ }
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ viewBinding {
+ enabled = true
+ }
+}
+dependencies {
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.9.0'
+
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+
+ implementation 'com.google.code.gson:gson:2.9.1'
+
+ implementation 'com.squareup.okhttp3:okhttp:4.11.0'
+ //图片处理
+ implementation 'com.github.bumptech.glide:glide:4.16.0'
+
+ implementation "androidx.palette:palette-ktx:1.0.0"
+// 废弃的歌词组件
+// implementation 'com.github.wangchenyan:lrcview:2.2.1'
+ implementation 'com.google.android.flexbox:flexbox:3.0.0'
+
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+
+//修改音乐标签库
+ implementation 'com.mpatric:mp3agic:0.9.1'
+
+// 沉浸式状态栏
+ implementation project(path: ':StatusBarUtil')
+// api project(path: ':lrcview')
+ //歌词组件库
+ api "com.github.cy745:EaseView:e11c3208a9"
+ api "androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03"
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3d281b3
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/about.html b/app/src/main/assets/about.html
new file mode 100644
index 0000000..d2b563a
--- /dev/null
+++ b/app/src/main/assets/about.html
@@ -0,0 +1,56 @@
+
开源在线音乐播放器项目
+该项目由作者MUQING编写,采用了 JAVA 设计语言,该应用对接了网易云音乐API,为用户提供了一系列功能丰富的服务:
+
+ - 音乐搜索与发现:
+
+ - 用户可以通过搜索框快速查找网易云音乐曲库中的歌曲、歌单。
+
+
+ - 在线播放与下载:
+
+ - 支持在线流媒体播放,用户可以流畅地收听高品质音乐,并可根据网络状况切换不同音质。
+ - 提供音乐下载功能,用户可将喜欢的歌曲保存到本地进行离线播放。
+
+
+ - 个性化歌单与收藏:
+
+ - 用户能收藏自己的歌单,以及收藏其他用户的优秀歌单。
+
+
+
+ - 歌词显示与同步:
+
+ - 在播放音乐时,同步展示歌词,并支持歌词滚动与定位。
+
+
+ - 账号绑定与同步:
+
+ - 用户可以使用网易云音乐账号登录,实现离线歌单和播放历史。
+
+
+
+
+
+ - 智能推荐系统:
+
+ - 根据用户的听歌习惯和偏好,推送个性化的音乐推荐。
+
+
+ - 后台播放与控制:
+
+ - 支持通知栏控制和小部件操作,即使在其他应用或屏幕关闭状态下也能方便控制音乐播放。
+
+
+
+通过这个开源项目,开发者学习如何对接第三方API实现音乐播放功能,还能了解各种小功能和细节的实现。
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/ColorCircle.java b/app/src/main/java/com/colorpicker/ColorCircle.java
new file mode 100644
index 0000000..f08d440
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/ColorCircle.java
@@ -0,0 +1,54 @@
+package com.colorpicker;
+
+import android.graphics.Color;
+
+public class ColorCircle {
+ private float x, y;
+ private float[] hsv = new float[3];
+ private float[] hsvClone;
+ private int color;
+
+ public ColorCircle(float x, float y, float[] hsv) {
+ set(x, y, hsv);
+ }
+
+ public double sqDist(float x, float y) {
+ double dx = this.x - x;
+ double dy = this.y - y;
+ return dx * dx + dy * dy;
+ }
+
+ public float getX() {
+ return x;
+ }
+
+ public float getY() {
+ return y;
+ }
+
+ public float[] getHsv() {
+ return hsv;
+ }
+
+ public float[] getHsvWithLightness(float lightness) {
+ if (hsvClone == null)
+ hsvClone = hsv.clone();
+ hsvClone[0] = hsv[0];
+ hsvClone[1] = hsv[1];
+ hsvClone[2] = lightness;
+ return hsvClone;
+ }
+
+ public void set(float x, float y, float[] hsv) {
+ this.x = x;
+ this.y = y;
+ this.hsv[0] = hsv[0];
+ this.hsv[1] = hsv[1];
+ this.hsv[2] = hsv[2];
+ this.color = Color.HSVToColor(this.hsv);
+ }
+
+ public int getColor() {
+ return color;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/ColorCircleDrawable.java b/app/src/main/java/com/colorpicker/ColorCircleDrawable.java
new file mode 100644
index 0000000..97df2b7
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/ColorCircleDrawable.java
@@ -0,0 +1,39 @@
+package com.colorpicker;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.ColorDrawable;
+
+import com.colorpicker.builder.PaintBuilder;
+
+public class ColorCircleDrawable extends ColorDrawable {
+ private float strokeWidth;
+ private Paint strokePaint = PaintBuilder.newPaint().style(Paint.Style.STROKE).stroke(strokeWidth).color(0xff9e9e9e).build();
+ private Paint fillPaint = PaintBuilder.newPaint().style(Paint.Style.FILL).color(0).build();
+ private Paint fillBackPaint = PaintBuilder.newPaint().shader(PaintBuilder.createAlphaPatternShader(26)).build();
+
+ public ColorCircleDrawable(int color) {
+ super(color);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.drawColor(0);
+
+ int width = canvas.getWidth();
+ float radius = width / 2f;
+ strokeWidth = radius / 8f;
+
+ this.strokePaint.setStrokeWidth(strokeWidth);
+ this.fillPaint.setColor(getColor());
+ canvas.drawCircle(radius, radius, radius - strokeWidth, fillBackPaint);
+ canvas.drawCircle(radius, radius, radius - strokeWidth, fillPaint);
+ canvas.drawCircle(radius, radius, radius - strokeWidth, strokePaint);
+ }
+
+ @Override
+ public void setColor(int color) {
+ super.setColor(color);
+ invalidateSelf();
+ }
+}
diff --git a/app/src/main/java/com/colorpicker/ColorPickerPreference.java b/app/src/main/java/com/colorpicker/ColorPickerPreference.java
new file mode 100644
index 0000000..d1902e1
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/ColorPickerPreference.java
@@ -0,0 +1,156 @@
+package com.colorpicker;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.preference.Preference;
+import androidx.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.colorpicker.builder.ColorPickerClickListener;
+import com.colorpicker.builder.ColorPickerDialogBuilder;
+import com.muqingbfq.R;
+
+public class ColorPickerPreference extends Preference {
+
+ protected boolean alphaSlider;
+ protected boolean lightSlider;
+ protected boolean border;
+
+ protected int selectedColor = 0;
+
+ protected ColorPickerView.WHEEL_TYPE wheelType;
+ protected int density;
+
+ private boolean pickerColorEdit;
+ private String pickerTitle;
+ private String pickerButtonCancel;
+ private String pickerButtonOk;
+
+ protected ImageView colorIndicator;
+
+ public ColorPickerPreference(Context context) {
+ super(context);
+ }
+
+ public ColorPickerPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initWith(context, attrs);
+ }
+
+ public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initWith(context, attrs);
+ }
+
+ private void initWith(Context context, AttributeSet attrs) {
+ final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerPreference);
+
+ try {
+ alphaSlider = typedArray.getBoolean(R.styleable.ColorPickerPreference_alphaSlider, false);
+ lightSlider = typedArray.getBoolean(R.styleable.ColorPickerPreference_lightnessSlider, false);
+ border = typedArray.getBoolean(R.styleable.ColorPickerPreference_border, true);
+
+ density = typedArray.getInt(R.styleable.ColorPickerPreference_density, 8);
+ wheelType = ColorPickerView.WHEEL_TYPE.indexOf(typedArray.getInt(R.styleable.ColorPickerPreference_wheelType, 0));
+
+ selectedColor = typedArray.getInt(R.styleable.ColorPickerPreference_initialColor, 0xffffffff);
+
+ pickerColorEdit = typedArray.getBoolean(R.styleable.ColorPickerPreference_pickerColorEdit, true);
+ pickerTitle = typedArray.getString(R.styleable.ColorPickerPreference_pickerTitle);
+ if (pickerTitle==null)
+ pickerTitle = "Choose color";
+
+ pickerButtonCancel = typedArray.getString(R.styleable.ColorPickerPreference_pickerButtonCancel);
+ if (pickerButtonCancel==null)
+ pickerButtonCancel = "cancel";
+
+ pickerButtonOk = typedArray.getString(R.styleable.ColorPickerPreference_pickerButtonOk);
+ if (pickerButtonOk==null)
+ pickerButtonOk = "ok";
+
+ } finally {
+ typedArray.recycle();
+ }
+
+ setWidgetLayoutResource(R.layout.color_widget);
+ }
+
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ int tmpColor = isEnabled()
+ ? selectedColor
+ : darken(selectedColor, .5f);
+
+ colorIndicator = (ImageView) view.findViewById(R.id.color_indicator);
+
+ ColorCircleDrawable colorChoiceDrawable = null;
+ Drawable currentDrawable = colorIndicator.getDrawable();
+ if (currentDrawable != null && currentDrawable instanceof ColorCircleDrawable)
+ colorChoiceDrawable = (ColorCircleDrawable) currentDrawable;
+
+ if (colorChoiceDrawable == null)
+ colorChoiceDrawable = new ColorCircleDrawable(tmpColor);
+
+ colorIndicator.setImageDrawable(colorChoiceDrawable);
+ }
+
+ public void setValue(int value) {
+ if (callChangeListener(value)) {
+ selectedColor = value;
+ persistInt(value);
+ notifyChanged();
+ }
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setValue(restoreValue ? getPersistedInt(0) : (Integer) defaultValue);
+ }
+
+ @Override
+ protected void onClick() {
+ ColorPickerDialogBuilder builder = ColorPickerDialogBuilder
+ .with(getContext())
+ .setTitle(pickerTitle)
+ .initialColor(selectedColor)
+ .showBorder(border)
+ .wheelType(wheelType)
+ .density(density)
+ .showColorEdit(pickerColorEdit)
+ .setPositiveButton(pickerButtonOk, new ColorPickerClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int selectedColorFromPicker, Integer[] allColors) {
+ setValue(selectedColorFromPicker);
+ }
+ })
+ .setNegativeButton(pickerButtonCancel, null);
+
+ if (!alphaSlider && !lightSlider) builder.noSliders();
+ else if (!alphaSlider) builder.lightnessSliderOnly();
+ else if (!lightSlider) builder.alphaSliderOnly();
+
+ builder
+ .build()
+ .show();
+ }
+
+ public static int darken(int color, float factor) {
+ int a = Color.alpha(color);
+ int r = Color.red(color);
+ int g = Color.green(color);
+ int b = Color.blue(color);
+
+ return Color.argb(a,
+ Math.max((int)(r * factor), 0),
+ Math.max((int)(g * factor), 0),
+ Math.max((int)(b * factor), 0));
+ }
+}
diff --git a/app/src/main/java/com/colorpicker/ColorPickerView.java b/app/src/main/java/com/colorpicker/ColorPickerView.java
new file mode 100644
index 0000000..7a9521f
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/ColorPickerView.java
@@ -0,0 +1,572 @@
+package com.colorpicker;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.colorpicker.builder.ColorWheelRendererBuilder;
+import com.colorpicker.builder.PaintBuilder;
+import com.colorpicker.renderer.ColorWheelRenderOption;
+import com.colorpicker.renderer.ColorWheelRenderer;
+import com.colorpicker.slider.AlphaSlider;
+import com.colorpicker.slider.LightnessSlider;
+import com.muqingbfq.R;
+
+import java.util.ArrayList;
+public class ColorPickerView extends View {
+ private static final float STROKE_RATIO = 1.5f;
+
+ private Bitmap colorWheel;
+ private Canvas colorWheelCanvas;
+ private Bitmap currentColor;
+ private Canvas currentColorCanvas;
+ private boolean showBorder;
+ private int density = 8;
+
+ private float lightness = 1;
+ private float alpha = 1;
+ private int backgroundColor = 0x00000000;
+
+ private Integer initialColors[] = new Integer[]{null, null, null, null, null};
+ private int colorSelection = 0;
+ private Integer initialColor;
+ private Integer pickerColorEditTextColor;
+ private Paint colorWheelFill = PaintBuilder.newPaint().color(0).build();
+ private Paint selectorStroke = PaintBuilder.newPaint().color(0).build();
+ private Paint alphaPatternPaint = PaintBuilder.newPaint().build();
+ private ColorCircle currentColorCircle;
+
+ private ArrayList colorChangedListeners = new ArrayList();
+ private ArrayList listeners = new ArrayList();
+
+ private LightnessSlider lightnessSlider;
+ private AlphaSlider alphaSlider;
+ private EditText colorEdit;
+ private TextWatcher colorTextChange = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ try {
+ int color = Color.parseColor(s.toString());
+
+ // set the color without changing the edit text preventing stack overflow
+ setColor(color, false);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ };
+ private LinearLayout colorPreview;
+
+ private ColorWheelRenderer renderer;
+
+ private int alphaSliderViewId, lightnessSliderViewId;
+
+ public ColorPickerView(Context context) {
+ super(context);
+ initWith(context, null);
+ }
+
+ public ColorPickerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initWith(context, attrs);
+ }
+
+ public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initWith(context, attrs);
+ }
+
+ @TargetApi(21)
+ public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initWith(context, attrs);
+ }
+
+ private void initWith(Context context, AttributeSet attrs) {
+ final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerPreference);
+
+ density = typedArray.getInt(R.styleable.ColorPickerPreference_density, 10);
+ initialColor = typedArray.getInt(R.styleable.ColorPickerPreference_initialColor, 0xffffffff);
+
+ pickerColorEditTextColor = typedArray.getInt(R.styleable.ColorPickerPreference_pickerColorEditTextColor, 0xffffffff);
+
+ WHEEL_TYPE wheelType = WHEEL_TYPE.indexOf(typedArray.getInt(R.styleable.ColorPickerPreference_wheelType, 0));
+ ColorWheelRenderer renderer = ColorWheelRendererBuilder.getRenderer(wheelType);
+
+ alphaSliderViewId = typedArray.getResourceId(R.styleable.ColorPickerPreference_alphaSliderView, 0);
+ lightnessSliderViewId = typedArray.getResourceId(R.styleable.ColorPickerPreference_lightnessSliderView, 0);
+
+ setRenderer(renderer);
+ setDensity(density);
+ setInitialColor(initialColor, true);
+
+ typedArray.recycle();
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+ updateColorWheel();
+ currentColorCircle = findNearestByColor(initialColor);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (alphaSliderViewId != 0)
+ setAlphaSlider((AlphaSlider) getRootView().findViewById(alphaSliderViewId));
+ if (lightnessSliderViewId != 0)
+ setLightnessSlider((LightnessSlider) getRootView().findViewById(lightnessSliderViewId));
+
+ updateColorWheel();
+ currentColorCircle = findNearestByColor(initialColor);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateColorWheel();
+ }
+
+ private void updateColorWheel() {
+ int width = getMeasuredWidth();
+ int height = getMeasuredHeight();
+
+ if (height < width)
+ width = height;
+ if (width <= 0)
+ return;
+ if (colorWheel == null || colorWheel.getWidth() != width) {
+ colorWheel = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
+ colorWheelCanvas = new Canvas(colorWheel);
+ alphaPatternPaint.setShader(PaintBuilder.createAlphaPatternShader(26));
+ }
+ if (currentColor == null || currentColor.getWidth() != width) {
+ currentColor = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
+ currentColorCanvas = new Canvas(currentColor);
+ }
+ drawColorWheel();
+ invalidate();
+ }
+
+ private void drawColorWheel() {
+ colorWheelCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ currentColorCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
+
+ if (renderer == null) return;
+
+ float half = colorWheelCanvas.getWidth() / 2f;
+ float strokeWidth = STROKE_RATIO * (1f + ColorWheelRenderer.GAP_PERCENTAGE);
+ float maxRadius = half - strokeWidth - half / density;
+ float cSize = maxRadius / (density - 1) / 2;
+
+ ColorWheelRenderOption colorWheelRenderOption = renderer.getRenderOption();
+ colorWheelRenderOption.density = this.density;
+ colorWheelRenderOption.maxRadius = maxRadius;
+ colorWheelRenderOption.cSize = cSize;
+ colorWheelRenderOption.strokeWidth = strokeWidth;
+ colorWheelRenderOption.alpha = alpha;
+ colorWheelRenderOption.lightness = lightness;
+ colorWheelRenderOption.targetCanvas = colorWheelCanvas;
+
+ renderer.initWith(colorWheelRenderOption);
+ renderer.draw();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int width = 0;
+ if (widthMode == MeasureSpec.UNSPECIFIED)
+ width = widthMeasureSpec;
+ else if (widthMode == MeasureSpec.AT_MOST)
+ width = MeasureSpec.getSize(widthMeasureSpec);
+ else if (widthMode == MeasureSpec.EXACTLY)
+ width = MeasureSpec.getSize(widthMeasureSpec);
+
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int height = 0;
+ if (heightMode == MeasureSpec.UNSPECIFIED)
+ height = heightMeasureSpec;
+ else if (heightMode == MeasureSpec.AT_MOST)
+ height = MeasureSpec.getSize(heightMeasureSpec);
+ else if (heightMode == MeasureSpec.EXACTLY)
+ height = MeasureSpec.getSize(heightMeasureSpec);
+ int squareDimen = width;
+ if (height < width)
+ squareDimen = height;
+ setMeasuredDimension(squareDimen, squareDimen);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE: {
+ int lastSelectedColor = getSelectedColor();
+ currentColorCircle = findNearestByPosition(event.getX(), event.getY());
+ int selectedColor = getSelectedColor();
+
+ callOnColorChangedListeners(lastSelectedColor, selectedColor);
+
+ initialColor = selectedColor;
+ setColorToSliders(selectedColor);
+ updateColorWheel();
+ invalidate();
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ int selectedColor = getSelectedColor();
+ if (listeners != null) {
+ for (OnColorSelectedListener listener : listeners) {
+ try {
+ listener.onColorSelected(selectedColor);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ setColorToSliders(selectedColor);
+ setColorText(selectedColor);
+ setColorPreviewColor(selectedColor);
+ invalidate();
+ break;
+ }
+ }
+ return true;
+ }
+
+ protected void callOnColorChangedListeners(int oldColor, int newColor) {
+ if (colorChangedListeners != null && oldColor != newColor) {
+ for (OnColorChangedListener listener : colorChangedListeners) {
+ try {
+ listener.onColorChanged(newColor);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.drawColor(backgroundColor);
+
+ float maxRadius = canvas.getWidth() / (1f + ColorWheelRenderer.GAP_PERCENTAGE);
+ float size = maxRadius / density / 2;
+ if (colorWheel != null && currentColorCircle != null) {
+ colorWheelFill.setColor(Color.HSVToColor(currentColorCircle.getHsvWithLightness(this.lightness)));
+ colorWheelFill.setAlpha((int) (alpha * 0xff));
+
+ // a separate canvas is used to erase an issue with the alpha pattern around the edges
+ // draw circle slightly larger than it needs to be, then erase edges to proper dimensions
+ currentColorCanvas.drawCircle(currentColorCircle.getX(), currentColorCircle.getY(), size + 4, alphaPatternPaint);
+ currentColorCanvas.drawCircle(currentColorCircle.getX(), currentColorCircle.getY(), size + 4, colorWheelFill);
+
+ selectorStroke = PaintBuilder.newPaint().color(0xffffffff).style(Paint.Style.STROKE).stroke(size * (STROKE_RATIO - 1)).xPerMode(PorterDuff.Mode.CLEAR).build();
+
+ if (showBorder) colorWheelCanvas.drawCircle(currentColorCircle.getX(), currentColorCircle.getY(), size + (selectorStroke.getStrokeWidth() / 2f), selectorStroke);
+ canvas.drawBitmap(colorWheel, 0, 0, null);
+
+ currentColorCanvas.drawCircle(currentColorCircle.getX(), currentColorCircle.getY(), size + (selectorStroke.getStrokeWidth() / 2f), selectorStroke);
+ canvas.drawBitmap(currentColor, 0, 0, null);
+ }
+ }
+
+ private ColorCircle findNearestByPosition(float x, float y) {
+ ColorCircle near = null;
+ double minDist = Double.MAX_VALUE;
+
+ for (ColorCircle colorCircle : renderer.getColorCircleList()) {
+ double dist = colorCircle.sqDist(x, y);
+ if (minDist > dist) {
+ minDist = dist;
+ near = colorCircle;
+ }
+ }
+
+ return near;
+ }
+
+ private ColorCircle findNearestByColor(int color) {
+ float[] hsv = new float[3];
+ Color.colorToHSV(color, hsv);
+ ColorCircle near = null;
+ double minDiff = Double.MAX_VALUE;
+ double x = hsv[1] * Math.cos(hsv[0] * Math.PI / 180);
+ double y = hsv[1] * Math.sin(hsv[0] * Math.PI / 180);
+
+ for (ColorCircle colorCircle : renderer.getColorCircleList()) {
+ float[] hsv1 = colorCircle.getHsv();
+ double x1 = hsv1[1] * Math.cos(hsv1[0] * Math.PI / 180);
+ double y1 = hsv1[1] * Math.sin(hsv1[0] * Math.PI / 180);
+ double dx = x - x1;
+ double dy = y - y1;
+ double dist = dx * dx + dy * dy;
+ if (dist < minDiff) {
+ minDiff = dist;
+ near = colorCircle;
+ }
+ }
+
+ return near;
+ }
+
+ public int getSelectedColor() {
+ int color = 0;
+ if (currentColorCircle != null)
+ color = Utils.colorAtLightness(currentColorCircle.getColor(), this.lightness);
+ return Utils.adjustAlpha(this.alpha, color);
+ }
+
+ public Integer[] getAllColors() {
+ return initialColors;
+ }
+
+ public void setInitialColors(Integer[] colors, int selectedColor) {
+ this.initialColors = colors;
+ this.colorSelection = selectedColor;
+ Integer initialColor = this.initialColors[this.colorSelection];
+ if (initialColor == null) initialColor = 0xffffffff;
+ setInitialColor(initialColor, true);
+ }
+
+ public void setInitialColor(int color, boolean updateText) {
+ float[] hsv = new float[3];
+ Color.colorToHSV(color, hsv);
+
+ this.alpha = Utils.getAlphaPercent(color);
+ this.lightness = hsv[2];
+ this.initialColors[this.colorSelection] = color;
+ this.initialColor = color;
+ setColorPreviewColor(color);
+ setColorToSliders(color);
+ if (this.colorEdit != null && updateText)
+ setColorText(color);
+ currentColorCircle = findNearestByColor(color);
+ }
+
+ public void setLightness(float lightness) {
+ int lastSelectedColor = getSelectedColor();
+
+ this.lightness = lightness;
+ if (currentColorCircle != null) {
+ this.initialColor = Color.HSVToColor(Utils.alphaValueAsInt(this.alpha), currentColorCircle.getHsvWithLightness(lightness));
+ if (this.colorEdit != null)
+ this.colorEdit.setText(Utils.getHexString(this.initialColor, this.alphaSlider != null));
+ if (this.alphaSlider != null && this.initialColor != null)
+ this.alphaSlider.setColor(this.initialColor);
+
+ callOnColorChangedListeners(lastSelectedColor, this.initialColor);
+
+ updateColorWheel();
+ invalidate();
+ }
+ }
+
+ public void setColor(int color, boolean updateText) {
+ setInitialColor(color, updateText);
+ updateColorWheel();
+ invalidate();
+ }
+
+ public void setAlphaValue(float alpha) {
+ int lastSelectedColor = getSelectedColor();
+
+ this.alpha = alpha;
+ this.initialColor = Color.HSVToColor(Utils.alphaValueAsInt(this.alpha), currentColorCircle.getHsvWithLightness(this.lightness));
+ if (this.colorEdit != null)
+ this.colorEdit.setText(Utils.getHexString(this.initialColor, this.alphaSlider != null));
+ if (this.lightnessSlider != null && this.initialColor != null)
+ this.lightnessSlider.setColor(this.initialColor);
+
+ callOnColorChangedListeners(lastSelectedColor, this.initialColor);
+
+ updateColorWheel();
+ invalidate();
+ }
+
+ public void addOnColorChangedListener(OnColorChangedListener listener) {
+ this.colorChangedListeners.add(listener);
+ }
+
+ public void addOnColorSelectedListener(OnColorSelectedListener listener) {
+ this.listeners.add(listener);
+ }
+
+ public void setLightnessSlider(LightnessSlider lightnessSlider) {
+ this.lightnessSlider = lightnessSlider;
+ if (lightnessSlider != null) {
+ this.lightnessSlider.setColorPicker(this);
+ this.lightnessSlider.setColor(getSelectedColor());
+ }
+ }
+
+ public void setAlphaSlider(AlphaSlider alphaSlider) {
+ this.alphaSlider = alphaSlider;
+ if (alphaSlider != null) {
+ this.alphaSlider.setColorPicker(this);
+ this.alphaSlider.setColor(getSelectedColor());
+ }
+ }
+
+ public void setColorEdit(EditText colorEdit) {
+ this.colorEdit = colorEdit;
+ if (this.colorEdit != null) {
+ this.colorEdit.setVisibility(View.VISIBLE);
+ this.colorEdit.addTextChangedListener(colorTextChange);
+ setColorEditTextColor(pickerColorEditTextColor);
+ }
+ }
+
+ public void setColorEditTextColor(int argb) {
+ this.pickerColorEditTextColor = argb;
+ if (colorEdit != null)
+ colorEdit.setTextColor(argb);
+ }
+
+ public void setDensity(int density) {
+ this.density = Math.max(2, density);
+ invalidate();
+ }
+
+ public void setRenderer(ColorWheelRenderer renderer) {
+ this.renderer = renderer;
+ invalidate();
+ }
+
+ public void setColorPreview(LinearLayout colorPreview, Integer selectedColor) {
+ if (colorPreview == null)
+ return;
+ this.colorPreview = colorPreview;
+ if (selectedColor == null)
+ selectedColor = 0;
+ int children = colorPreview.getChildCount();
+ if (children == 0 || colorPreview.getVisibility() != View.VISIBLE)
+ return;
+
+ for (int i = 0; i < children; i++) {
+ View childView = colorPreview.getChildAt(i);
+ if (!(childView instanceof LinearLayout))
+ continue;
+ LinearLayout childLayout = (LinearLayout) childView;
+ if (i == selectedColor) {
+ childLayout.setBackgroundColor(Color.WHITE);
+ }
+ ImageView childImage = (ImageView) childLayout.findViewById(R.id.image_preview);
+ childImage.setClickable(true);
+ childImage.setTag(i);
+ childImage.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (v == null)
+ return;
+ Object tag = v.getTag();
+ if (tag == null || !(tag instanceof Integer))
+ return;
+ setSelectedColor((int) tag);
+ }
+ });
+ }
+ }
+
+ public void setSelectedColor(int previewNumber) {
+ if (initialColors == null || initialColors.length < previewNumber)
+ return;
+ this.colorSelection = previewNumber;
+ setHighlightedColor(previewNumber);
+ Integer color = initialColors[previewNumber];
+ if (color == null)
+ return;
+ setColor(color, true);
+ }
+
+ public void setShowBorder(boolean showBorder) {
+ this.showBorder = showBorder;
+ }
+
+ private void setHighlightedColor(int previewNumber) {
+ int children = colorPreview.getChildCount();
+ if (children == 0 || colorPreview.getVisibility() != View.VISIBLE)
+ return;
+
+ for (int i = 0; i < children; i++) {
+ View childView = colorPreview.getChildAt(i);
+ if (!(childView instanceof LinearLayout))
+ continue;
+ LinearLayout childLayout = (LinearLayout) childView;
+ if (i == previewNumber) {
+ childLayout.setBackgroundColor(Color.WHITE);
+ } else {
+ childLayout.setBackgroundColor(Color.TRANSPARENT);
+ }
+ }
+ }
+
+ private void setColorPreviewColor(int newColor) {
+ if (colorPreview == null || initialColors == null || colorSelection > initialColors.length || initialColors[colorSelection] == null)
+ return;
+
+ int children = colorPreview.getChildCount();
+ if (children == 0 || colorPreview.getVisibility() != View.VISIBLE)
+ return;
+
+ View childView = colorPreview.getChildAt(colorSelection);
+ if (!(childView instanceof LinearLayout))
+ return;
+ LinearLayout childLayout = (LinearLayout) childView;
+ ImageView childImage = (ImageView) childLayout.findViewById(R.id.image_preview);
+ childImage.setImageDrawable(new ColorCircleDrawable(newColor));
+ }
+
+ private void setColorText(int argb) {
+ if (colorEdit == null)
+ return;
+ colorEdit.setText(Utils.getHexString(argb, this.alphaSlider != null));
+ }
+
+ private void setColorToSliders(int selectedColor) {
+ if (lightnessSlider != null)
+ lightnessSlider.setColor(selectedColor);
+ if (alphaSlider != null)
+ alphaSlider.setColor(selectedColor);
+ }
+
+ public enum WHEEL_TYPE {
+ FLOWER, CIRCLE;
+
+ public static WHEEL_TYPE indexOf(int index) {
+ switch (index) {
+ case 0:
+ return FLOWER;
+ case 1:
+ return CIRCLE;
+ }
+ return FLOWER;
+ }
+ }
+}
diff --git a/app/src/main/java/com/colorpicker/OnColorChangedListener.java b/app/src/main/java/com/colorpicker/OnColorChangedListener.java
new file mode 100644
index 0000000..ab2d209
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/OnColorChangedListener.java
@@ -0,0 +1,5 @@
+package com.colorpicker;
+
+public interface OnColorChangedListener {
+ void onColorChanged(int selectedColor);
+}
diff --git a/app/src/main/java/com/colorpicker/OnColorSelectedListener.java b/app/src/main/java/com/colorpicker/OnColorSelectedListener.java
new file mode 100644
index 0000000..a4213f9
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/OnColorSelectedListener.java
@@ -0,0 +1,5 @@
+package com.colorpicker;
+
+public interface OnColorSelectedListener {
+ void onColorSelected(int selectedColor);
+}
diff --git a/app/src/main/java/com/colorpicker/Utils.java b/app/src/main/java/com/colorpicker/Utils.java
new file mode 100644
index 0000000..94ba331
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/Utils.java
@@ -0,0 +1,40 @@
+package com.colorpicker;
+
+import android.graphics.Color;
+
+/**
+ * Created by Charles Andersons on 4/17/15.
+ */
+public class Utils {
+ public static float getAlphaPercent(int argb) {
+ return Color.alpha(argb) / 255f;
+ }
+
+ public static int alphaValueAsInt(float alpha) {
+ return Math.round(alpha * 255);
+ }
+
+ public static int adjustAlpha(float alpha, int color) {
+ return alphaValueAsInt(alpha) << 24 | (0x00ffffff & color);
+ }
+
+ public static int colorAtLightness(int color, float lightness) {
+ float[] hsv = new float[3];
+ Color.colorToHSV(color, hsv);
+ hsv[2] = lightness;
+ return Color.HSVToColor(hsv);
+ }
+
+ public static float lightnessOfColor(int color) {
+ float[] hsv = new float[3];
+ Color.colorToHSV(color, hsv);
+ return hsv[2];
+ }
+
+ public static String getHexString(int color, boolean showAlpha) {
+ int base = showAlpha ? 0xFFFFFFFF : 0xFFFFFF;
+ String format = showAlpha ? "#%08X" : "#%06X";
+ return String.format(format, (base & color)).toUpperCase();
+ }
+
+}
diff --git a/app/src/main/java/com/colorpicker/builder/ColorPickerClickListener.java b/app/src/main/java/com/colorpicker/builder/ColorPickerClickListener.java
new file mode 100644
index 0000000..f1e123a
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/builder/ColorPickerClickListener.java
@@ -0,0 +1,10 @@
+package com.colorpicker.builder;
+
+import android.content.DialogInterface;
+
+/**
+ * Created by Charles Anderson on 4/17/15.
+ */
+public interface ColorPickerClickListener {
+ void onClick(DialogInterface d, int lastSelectedColor, Integer[] allColors);
+}
diff --git a/app/src/main/java/com/colorpicker/builder/ColorPickerDialogBuilder.java b/app/src/main/java/com/colorpicker/builder/ColorPickerDialogBuilder.java
new file mode 100644
index 0000000..c7dbfde
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/builder/ColorPickerDialogBuilder.java
@@ -0,0 +1,297 @@
+package com.colorpicker.builder;
+
+import androidx.appcompat.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.text.InputFilter;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.colorpicker.ColorPickerView;
+import com.colorpicker.OnColorChangedListener;
+import com.colorpicker.OnColorSelectedListener;
+import com.colorpicker.Utils;
+import com.colorpicker.renderer.ColorWheelRenderer;
+import com.colorpicker.slider.AlphaSlider;
+import com.colorpicker.slider.LightnessSlider;
+import com.muqingbfq.R;
+
+public class ColorPickerDialogBuilder {
+ private MaterialAlertDialogBuilder builder;
+ private LinearLayout pickerContainer;
+ private ColorPickerView colorPickerView;
+ private LightnessSlider lightnessSlider;
+ private AlphaSlider alphaSlider;
+ private EditText colorEdit;
+ private LinearLayout colorPreview;
+
+ private boolean isLightnessSliderEnabled = true;
+ private boolean isAlphaSliderEnabled = true;
+ private boolean isBorderEnabled = true;
+ private boolean isColorEditEnabled = false;
+ private boolean isPreviewEnabled = false;
+ private int pickerCount = 1;
+ private int defaultMargin = 0;
+ private int defaultMarginTop = 0;
+ private Integer[] initialColor = new Integer[]{null, null, null, null, null};
+
+ private ColorPickerDialogBuilder(Context context) {
+ this(context, 0);
+ }
+
+ private ColorPickerDialogBuilder(Context context, int theme) {
+ defaultMargin = getDimensionAsPx(context, R.dimen.default_slider_margin);
+ defaultMarginTop = getDimensionAsPx(context, R.dimen.default_margin_top);
+
+ builder = new MaterialAlertDialogBuilder(context, theme);
+ pickerContainer = new LinearLayout(context);
+ pickerContainer.setOrientation(LinearLayout.VERTICAL);
+ pickerContainer.setGravity(Gravity.CENTER_HORIZONTAL);
+ pickerContainer.setPadding(defaultMargin, defaultMarginTop, defaultMargin, 0);
+
+ LinearLayout.LayoutParams layoutParamsForColorPickerView = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
+ layoutParamsForColorPickerView.weight = 1;
+ colorPickerView = new ColorPickerView(context);
+
+ pickerContainer.addView(colorPickerView, layoutParamsForColorPickerView);
+
+ builder.setView(pickerContainer);
+ }
+
+ public static ColorPickerDialogBuilder with(Context context) {
+ return new ColorPickerDialogBuilder(context);
+ }
+
+ public static ColorPickerDialogBuilder with(Context context, int theme) {
+ return new ColorPickerDialogBuilder(context, theme);
+ }
+
+ public ColorPickerDialogBuilder setTitle(String title) {
+ builder.setTitle(title);
+ return this;
+ }
+
+ public ColorPickerDialogBuilder setTitle(int titleId) {
+ builder.setTitle(titleId);
+ return this;
+ }
+
+ public ColorPickerDialogBuilder initialColor(int initialColor) {
+ this.initialColor[0] = initialColor;
+ return this;
+ }
+
+ public ColorPickerDialogBuilder initialColors(int[] initialColor) {
+ for (int i = 0; i < initialColor.length && i < this.initialColor.length; i++) {
+ this.initialColor[i] = initialColor[i];
+ }
+ return this;
+ }
+
+ public ColorPickerDialogBuilder wheelType(ColorPickerView.WHEEL_TYPE wheelType) {
+ ColorWheelRenderer renderer = ColorWheelRendererBuilder.getRenderer(wheelType);
+ colorPickerView.setRenderer(renderer);
+ return this;
+ }
+
+ public ColorPickerDialogBuilder density(int density) {
+ colorPickerView.setDensity(density);
+ return this;
+ }
+
+ public ColorPickerDialogBuilder setOnColorChangedListener(OnColorChangedListener onColorChangedListener) {
+ colorPickerView.addOnColorChangedListener(onColorChangedListener);
+ return this;
+ }
+
+ public ColorPickerDialogBuilder setOnColorSelectedListener(OnColorSelectedListener onColorSelectedListener) {
+ colorPickerView.addOnColorSelectedListener(onColorSelectedListener);
+ return this;
+ }
+
+ public ColorPickerDialogBuilder setPositiveButton(CharSequence text, final ColorPickerClickListener onClickListener) {
+ builder.setPositiveButton(text, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ positiveButtonOnClick(dialog, onClickListener);
+ }
+ });
+ return this;
+ }
+
+ public ColorPickerDialogBuilder setPositiveButton(int textId, final ColorPickerClickListener onClickListener) {
+ builder.setPositiveButton(textId, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ positiveButtonOnClick(dialog, onClickListener);
+ }
+ });
+ return this;
+ }
+
+ public ColorPickerDialogBuilder setNegativeButton(CharSequence text, DialogInterface.OnClickListener onClickListener) {
+ builder.setNegativeButton(text, onClickListener);
+ return this;
+ }
+
+ public ColorPickerDialogBuilder setNegativeButton(int textId, DialogInterface.OnClickListener onClickListener) {
+ builder.setNegativeButton(textId, onClickListener);
+ return this;
+ }
+
+ public ColorPickerDialogBuilder noSliders() {
+ isLightnessSliderEnabled = false;
+ isAlphaSliderEnabled = false;
+ return this;
+ }
+
+ public ColorPickerDialogBuilder alphaSliderOnly() {
+ isLightnessSliderEnabled = false;
+ isAlphaSliderEnabled = true;
+ return this;
+ }
+
+ public ColorPickerDialogBuilder lightnessSliderOnly() {
+ isLightnessSliderEnabled = true;
+ isAlphaSliderEnabled = false;
+ return this;
+ }
+
+ public ColorPickerDialogBuilder showAlphaSlider(boolean showAlpha) {
+ isAlphaSliderEnabled = showAlpha;
+ return this;
+ }
+
+ public ColorPickerDialogBuilder showLightnessSlider(boolean showLightness) {
+ isLightnessSliderEnabled = showLightness;
+ return this;
+ }
+
+ public ColorPickerDialogBuilder showBorder(boolean showBorder) {
+ isBorderEnabled = showBorder;
+ return this;
+ }
+
+ public ColorPickerDialogBuilder showColorEdit(boolean showEdit) {
+ isColorEditEnabled = showEdit;
+ return this;
+ }
+
+ public ColorPickerDialogBuilder setColorEditTextColor(int argb) {
+ colorPickerView.setColorEditTextColor(argb);
+ return this;
+ }
+
+ public ColorPickerDialogBuilder showColorPreview(boolean showPreview) {
+ isPreviewEnabled = showPreview;
+ if (!showPreview)
+ pickerCount = 1;
+ return this;
+ }
+
+ public ColorPickerDialogBuilder setPickerCount(int pickerCount) throws IndexOutOfBoundsException {
+ if (pickerCount < 1 || pickerCount > 5)
+ throw new IndexOutOfBoundsException("Picker Can Only Support 1-5 Colors");
+ this.pickerCount = pickerCount;
+ if (this.pickerCount > 1)
+ this.isPreviewEnabled = true;
+ return this;
+ }
+
+ public AlertDialog build() {
+ Context context = builder.getContext();
+ colorPickerView.setInitialColors(initialColor, getStartOffset(initialColor));
+ colorPickerView.setShowBorder(isBorderEnabled);
+
+ if (isLightnessSliderEnabled) {
+ LinearLayout.LayoutParams layoutParamsForLightnessBar = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getDimensionAsPx(context, R.dimen.default_slider_height));
+ lightnessSlider = new LightnessSlider(context);
+ lightnessSlider.setLayoutParams(layoutParamsForLightnessBar);
+ pickerContainer.addView(lightnessSlider);
+ colorPickerView.setLightnessSlider(lightnessSlider);
+ lightnessSlider.setColor(getStartColor(initialColor));
+ lightnessSlider.setShowBorder(isBorderEnabled);
+ }
+ if (isAlphaSliderEnabled) {
+ LinearLayout.LayoutParams layoutParamsForAlphaBar = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getDimensionAsPx(context, R.dimen.default_slider_height));
+ alphaSlider = new AlphaSlider(context);
+ alphaSlider.setLayoutParams(layoutParamsForAlphaBar);
+ pickerContainer.addView(alphaSlider);
+ colorPickerView.setAlphaSlider(alphaSlider);
+ alphaSlider.setColor(getStartColor(initialColor));
+ alphaSlider.setShowBorder(isBorderEnabled);
+ }
+ if (isColorEditEnabled) {
+ LinearLayout.LayoutParams layoutParamsForColorEdit = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ colorEdit = (EditText) View.inflate(context, R.layout.color_edit, null);
+ colorEdit.setFilters(new InputFilter[]{new InputFilter.AllCaps()});
+ colorEdit.setSingleLine();
+ colorEdit.setVisibility(View.GONE);
+
+ // limit number of characters to hexColors
+ int maxLength = isAlphaSliderEnabled ? 9 : 7;
+ colorEdit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
+
+ pickerContainer.addView(colorEdit, layoutParamsForColorEdit);
+
+ colorEdit.setText(Utils.getHexString(getStartColor(initialColor), isAlphaSliderEnabled));
+ colorPickerView.setColorEdit(colorEdit);
+ }
+ if (isPreviewEnabled) {
+ colorPreview = (LinearLayout) View.inflate(context, R.layout.color_preview, null);
+ colorPreview.setVisibility(View.GONE);
+ pickerContainer.addView(colorPreview);
+
+ if (initialColor.length == 0) {
+ ImageView colorImage = (ImageView) View.inflate(context, R.layout.color_selector, null);
+ colorImage.setImageDrawable(new ColorDrawable(Color.WHITE));
+ } else {
+ for (int i = 0; i < initialColor.length && i < this.pickerCount; i++) {
+ if (initialColor[i] == null)
+ break;
+ LinearLayout colorLayout = (LinearLayout) View.inflate(context, R.layout.color_selector, null);
+ ImageView colorImage = (ImageView) colorLayout.findViewById(R.id.image_preview);
+ colorImage.setImageDrawable(new ColorDrawable(initialColor[i]));
+ colorPreview.addView(colorLayout);
+ }
+ }
+ colorPreview.setVisibility(View.VISIBLE);
+ colorPickerView.setColorPreview(colorPreview, getStartOffset(initialColor));
+ }
+
+ return builder.create();
+ }
+
+ private Integer getStartOffset(Integer[] colors) {
+ Integer start = 0;
+ for (int i = 0; i < colors.length; i++) {
+ if (colors[i] == null) {
+ return start;
+ }
+ start = (i + 1) / 2;
+ }
+ return start;
+ }
+
+ private int getStartColor(Integer[] colors) {
+ Integer startColor = getStartOffset(colors);
+ return startColor == null ? Color.WHITE : colors[startColor];
+ }
+
+ private static int getDimensionAsPx(Context context, int rid) {
+ return (int) (context.getResources().getDimension(rid) + .5f);
+ }
+
+ private void positiveButtonOnClick(DialogInterface dialog, ColorPickerClickListener onClickListener) {
+ int selectedColor = colorPickerView.getSelectedColor();
+ Integer[] allColors = colorPickerView.getAllColors();
+ onClickListener.onClick(dialog, selectedColor, allColors);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/builder/ColorWheelRendererBuilder.java b/app/src/main/java/com/colorpicker/builder/ColorWheelRendererBuilder.java
new file mode 100644
index 0000000..dcde8ab
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/builder/ColorWheelRendererBuilder.java
@@ -0,0 +1,18 @@
+package com.colorpicker.builder;
+
+import com.colorpicker.ColorPickerView;
+import com.colorpicker.renderer.ColorWheelRenderer;
+import com.colorpicker.renderer.FlowerColorWheelRenderer;
+import com.colorpicker.renderer.SimpleColorWheelRenderer;
+
+public class ColorWheelRendererBuilder {
+ public static ColorWheelRenderer getRenderer(ColorPickerView.WHEEL_TYPE wheelType) {
+ switch (wheelType) {
+ case CIRCLE:
+ return new SimpleColorWheelRenderer();
+ case FLOWER:
+ return new FlowerColorWheelRenderer();
+ }
+ throw new IllegalArgumentException("wrong WHEEL_TYPE");
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/builder/PaintBuilder.java b/app/src/main/java/com/colorpicker/builder/PaintBuilder.java
new file mode 100644
index 0000000..31d2d4d
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/builder/PaintBuilder.java
@@ -0,0 +1,82 @@
+package com.colorpicker.builder;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Shader;
+
+public class PaintBuilder {
+ public static PaintHolder newPaint() {
+ return new PaintHolder();
+ }
+
+ public static class PaintHolder {
+ private Paint paint;
+
+ private PaintHolder() {
+ this.paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ }
+
+ public PaintHolder color(int color) {
+ this.paint.setColor(color);
+ return this;
+ }
+
+ public PaintHolder antiAlias(boolean flag) {
+ this.paint.setAntiAlias(flag);
+ return this;
+ }
+
+ public PaintHolder style(Paint.Style style) {
+ this.paint.setStyle(style);
+ return this;
+ }
+
+ public PaintHolder mode(PorterDuff.Mode mode) {
+ this.paint.setXfermode(new PorterDuffXfermode(mode));
+ return this;
+ }
+
+ public PaintHolder stroke(float width) {
+ this.paint.setStrokeWidth(width);
+ return this;
+ }
+
+ public PaintHolder xPerMode(PorterDuff.Mode mode) {
+ this.paint.setXfermode(new PorterDuffXfermode(mode));
+ return this;
+ }
+
+ public PaintHolder shader(Shader shader) {
+ this.paint.setShader(shader);
+ return this;
+ }
+
+ public Paint build() {
+ return this.paint;
+ }
+ }
+
+ public static Shader createAlphaPatternShader(int size) {
+ size /= 2;
+ size = Math.max(8, size * 2);
+ return new BitmapShader(createAlphaBackgroundPattern(size), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
+ }
+
+ private static Bitmap createAlphaBackgroundPattern(int size) {
+ Paint alphaPatternPaint = PaintBuilder.newPaint().build();
+ Bitmap bm = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(bm);
+ int s = Math.round(size / 2f);
+ for (int i = 0; i < 2; i++)
+ for (int j = 0; j < 2; j++) {
+ if ((i + j) % 2 == 0) alphaPatternPaint.setColor(0xffffffff);
+ else alphaPatternPaint.setColor(0xffd0d0d0);
+ c.drawRect(i * s, j * s, (i + 1) * s, (j + 1) * s, alphaPatternPaint);
+ }
+ return bm;
+ }
+}
diff --git a/app/src/main/java/com/colorpicker/renderer/AbsColorWheelRenderer.java b/app/src/main/java/com/colorpicker/renderer/AbsColorWheelRenderer.java
new file mode 100644
index 0000000..c6cfe9f
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/renderer/AbsColorWheelRenderer.java
@@ -0,0 +1,34 @@
+package com.colorpicker.renderer;
+
+import com.colorpicker.ColorCircle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class AbsColorWheelRenderer implements ColorWheelRenderer {
+ protected ColorWheelRenderOption colorWheelRenderOption;
+ protected List colorCircleList = new ArrayList<>();
+
+ public void initWith(ColorWheelRenderOption colorWheelRenderOption) {
+ this.colorWheelRenderOption = colorWheelRenderOption;
+ this.colorCircleList.clear();
+ }
+
+ @Override
+ public ColorWheelRenderOption getRenderOption() {
+ if (colorWheelRenderOption == null) colorWheelRenderOption = new ColorWheelRenderOption();
+ return colorWheelRenderOption;
+ }
+
+ public List getColorCircleList() {
+ return colorCircleList;
+ }
+
+ protected int getAlphaValueAsInt() {
+ return Math.round(colorWheelRenderOption.alpha * 255);
+ }
+
+ protected int calcTotalCount(float radius, float size) {
+ return Math.max(1, (int) ((1f - GAP_PERCENTAGE) * Math.PI / (Math.asin(size / radius)) + 0.5f));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/renderer/ColorWheelRenderOption.java b/app/src/main/java/com/colorpicker/renderer/ColorWheelRenderOption.java
new file mode 100644
index 0000000..0eda4cd
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/renderer/ColorWheelRenderOption.java
@@ -0,0 +1,10 @@
+package com.colorpicker.renderer;
+
+import android.graphics.Canvas;
+
+public class ColorWheelRenderOption {
+ public int density;
+ public float maxRadius;
+ public float cSize, strokeWidth, alpha, lightness;
+ public Canvas targetCanvas;
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/renderer/ColorWheelRenderer.java b/app/src/main/java/com/colorpicker/renderer/ColorWheelRenderer.java
new file mode 100644
index 0000000..0caab95
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/renderer/ColorWheelRenderer.java
@@ -0,0 +1,16 @@
+package com.colorpicker.renderer;
+
+import com.colorpicker.ColorCircle;
+import java.util.List;
+
+public interface ColorWheelRenderer {
+ float GAP_PERCENTAGE = 0.025f;
+
+ void draw();
+
+ ColorWheelRenderOption getRenderOption();
+
+ void initWith(ColorWheelRenderOption colorWheelRenderOption);
+
+ List getColorCircleList();
+}
diff --git a/app/src/main/java/com/colorpicker/renderer/FlowerColorWheelRenderer.java b/app/src/main/java/com/colorpicker/renderer/FlowerColorWheelRenderer.java
new file mode 100644
index 0000000..3eb60c8
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/renderer/FlowerColorWheelRenderer.java
@@ -0,0 +1,50 @@
+package com.colorpicker.renderer;
+
+import android.graphics.Color;
+import android.graphics.Paint;
+
+import com.colorpicker.ColorCircle;
+import com.colorpicker.builder.PaintBuilder;
+
+public class FlowerColorWheelRenderer extends AbsColorWheelRenderer {
+ private Paint selectorFill = PaintBuilder.newPaint().build();
+ private float[] hsv = new float[3];
+ private float sizeJitter = 1.2f;
+
+ @Override
+ public void draw() {
+ final int setSize = colorCircleList.size();
+ int currentCount = 0;
+ float half = colorWheelRenderOption.targetCanvas.getWidth() / 2f;
+ int density = colorWheelRenderOption.density;
+ float strokeWidth = colorWheelRenderOption.strokeWidth;
+ float maxRadius = colorWheelRenderOption.maxRadius;
+ float cSize = colorWheelRenderOption.cSize;
+
+ for (int i = 0; i < density; i++) {
+ float p = (float) i / (density - 1); // 0~1
+ float jitter = (i - density / 2f) / density; // -0.5 ~ 0.5
+ float radius = maxRadius * p;
+ float size = Math.max(1.5f + strokeWidth, cSize + (i == 0 ? 0 : cSize * sizeJitter * jitter));
+ int total = Math.min(calcTotalCount(radius, size), density * 2);
+
+ for (int j = 0; j < total; j++) {
+ double angle = Math.PI * 2 * j / total + (Math.PI / total) * ((i + 1) % 2);
+ float x = half + (float) (radius * Math.cos(angle));
+ float y = half + (float) (radius * Math.sin(angle));
+ hsv[0] = (float) (angle * 180 / Math.PI);
+ hsv[1] = radius / maxRadius;
+ hsv[2] = colorWheelRenderOption.lightness;
+ selectorFill.setColor(Color.HSVToColor(hsv));
+ selectorFill.setAlpha(getAlphaValueAsInt());
+
+ colorWheelRenderOption.targetCanvas.drawCircle(x, y, size - strokeWidth, selectorFill);
+
+ if (currentCount >= setSize) {
+ colorCircleList.add(new ColorCircle(x, y, hsv));
+ } else colorCircleList.get(currentCount).set(x, y, hsv);
+ currentCount++;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/renderer/SimpleColorWheelRenderer.java b/app/src/main/java/com/colorpicker/renderer/SimpleColorWheelRenderer.java
new file mode 100644
index 0000000..b4032d0
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/renderer/SimpleColorWheelRenderer.java
@@ -0,0 +1,45 @@
+package com.colorpicker.renderer;
+
+import android.graphics.Color;
+import android.graphics.Paint;
+
+import com.colorpicker.ColorCircle;
+import com.colorpicker.builder.PaintBuilder;
+public class SimpleColorWheelRenderer extends AbsColorWheelRenderer {
+ private Paint selectorFill = PaintBuilder.newPaint().build();
+ private float[] hsv = new float[3];
+
+ @Override
+ public void draw() {
+ final int setSize = colorCircleList.size();
+ int currentCount = 0;
+ float half = colorWheelRenderOption.targetCanvas.getWidth() / 2f;
+ int density = colorWheelRenderOption.density;
+ float maxRadius = colorWheelRenderOption.maxRadius;
+
+ for (int i = 0; i < density; i++) {
+ float p = (float) i / (density - 1); // 0~1
+ float radius = maxRadius * p;
+ float size = colorWheelRenderOption.cSize;
+ int total = calcTotalCount(radius, size);
+
+ for (int j = 0; j < total; j++) {
+ double angle = Math.PI * 2 * j / total + (Math.PI / total) * ((i + 1) % 2);
+ float x = half + (float) (radius * Math.cos(angle));
+ float y = half + (float) (radius * Math.sin(angle));
+ hsv[0] = (float) (angle * 180 / Math.PI);
+ hsv[1] = radius / maxRadius;
+ hsv[2] = colorWheelRenderOption.lightness;
+ selectorFill.setColor(Color.HSVToColor(hsv));
+ selectorFill.setAlpha(getAlphaValueAsInt());
+
+ colorWheelRenderOption.targetCanvas.drawCircle(x, y, size - colorWheelRenderOption.strokeWidth, selectorFill);
+
+ if (currentCount >= setSize)
+ colorCircleList.add(new ColorCircle(x, y, hsv));
+ else colorCircleList.get(currentCount).set(x, y, hsv);
+ currentCount++;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/slider/AbsCustomSlider.java b/app/src/main/java/com/colorpicker/slider/AbsCustomSlider.java
new file mode 100644
index 0000000..3418a77
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/slider/AbsCustomSlider.java
@@ -0,0 +1,189 @@
+package com.colorpicker.slider;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import androidx.annotation.DimenRes;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.muqingbfq.R;
+
+public abstract class AbsCustomSlider extends View {
+ protected Bitmap bitmap;
+ protected Canvas bitmapCanvas;
+ protected Bitmap bar;
+ protected Canvas barCanvas;
+ protected OnValueChangedListener onValueChangedListener;
+ protected int barOffsetX;
+ protected int handleRadius = 20;
+ protected int barHeight = 5;
+ protected float value = 1;
+ protected boolean showBorder = false;
+
+ private boolean inVerticalOrientation = false;
+
+ public AbsCustomSlider(Context context) {
+ super(context);
+ init(context, null);
+ }
+
+ public AbsCustomSlider(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public AbsCustomSlider(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ TypedArray styledAttrs = context.getTheme().obtainStyledAttributes(
+ attrs, R.styleable.AbsCustomSlider, 0, 0);
+ try {
+ inVerticalOrientation = styledAttrs.getBoolean(
+ R.styleable.AbsCustomSlider_inVerticalOrientation, inVerticalOrientation);
+ } finally {
+ styledAttrs.recycle();
+ }
+ }
+
+ protected void updateBar() {
+ handleRadius = getDimension(R.dimen.default_slider_handler_radius);
+ barHeight = getDimension(R.dimen.default_slider_bar_height);
+ barOffsetX = handleRadius;
+
+ if (bar == null)
+ createBitmaps();
+ drawBar(barCanvas);
+ invalidate();
+ }
+
+ protected void createBitmaps() {
+ int width;
+ int height;
+ if (inVerticalOrientation) {
+ width = getHeight();
+ height = getWidth();
+ } else {
+ width = getWidth();
+ height = getHeight();
+ }
+
+ bar = Bitmap.createBitmap(Math.max(width - barOffsetX * 2, 1), barHeight, Bitmap.Config.ARGB_8888);
+ barCanvas = new Canvas(bar);
+
+ if (bitmap == null || bitmap.getWidth() != width || bitmap.getHeight() != height) {
+ if (bitmap != null) bitmap.recycle();
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ bitmapCanvas = new Canvas(bitmap);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ int width;
+ int height;
+ if (inVerticalOrientation) {
+ width = getHeight();
+ height = getWidth();
+
+ canvas.rotate(-90);
+ canvas.translate(-width, 0);
+ } else {
+ width = getWidth();
+ height = getHeight();
+ }
+
+ if (bar != null && bitmapCanvas != null) {
+ bitmapCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ bitmapCanvas.drawBitmap(bar, barOffsetX, (height - bar.getHeight()) / 2, null);
+
+ float x = handleRadius + value * (width - handleRadius * 2);
+ float y = height / 2f;
+ drawHandle(bitmapCanvas, x, y);
+ canvas.drawBitmap(bitmap, 0, 0, null);
+ }
+ }
+
+ protected abstract void drawBar(Canvas barCanvas);
+
+ protected abstract void onValueChanged(float value);
+
+ protected abstract void drawHandle(Canvas canvas, float x, float y);
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateBar();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int width = 0;
+ if (widthMode == MeasureSpec.UNSPECIFIED)
+ width = widthMeasureSpec;
+ else if (widthMode == MeasureSpec.AT_MOST)
+ width = MeasureSpec.getSize(widthMeasureSpec);
+ else if (widthMode == MeasureSpec.EXACTLY)
+ width = MeasureSpec.getSize(widthMeasureSpec);
+
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int height = 0;
+ if (heightMode == MeasureSpec.UNSPECIFIED)
+ height = heightMeasureSpec;
+ else if (heightMode == MeasureSpec.AT_MOST)
+ height = MeasureSpec.getSize(heightMeasureSpec);
+ else if (heightMode == MeasureSpec.EXACTLY)
+ height = MeasureSpec.getSize(heightMeasureSpec);
+
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE: {
+ if (bar != null) {
+ if (inVerticalOrientation) {
+ value = 1 - (event.getY() - barOffsetX) / bar.getWidth();
+ } else {
+ value = (event.getX() - barOffsetX) / bar.getWidth();
+ }
+ value = Math.max(0, Math.min(value, 1));
+ onValueChanged(value);
+ invalidate();
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ onValueChanged(value);
+ if (onValueChangedListener != null)
+ onValueChangedListener.onValueChanged(value);
+ invalidate();
+ }
+ }
+ return true;
+ }
+
+ protected int getDimension(@DimenRes int id) {
+ return getResources().getDimensionPixelSize(id);
+ }
+
+ public void setShowBorder(boolean showBorder) {
+ this.showBorder = showBorder;
+ }
+
+ public void setOnValueChangedListener(OnValueChangedListener onValueChangedListener) {
+ this.onValueChangedListener = onValueChangedListener;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/slider/AlphaSlider.java b/app/src/main/java/com/colorpicker/slider/AlphaSlider.java
new file mode 100644
index 0000000..108e64c
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/slider/AlphaSlider.java
@@ -0,0 +1,99 @@
+package com.colorpicker.slider;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.util.AttributeSet;
+
+import com.colorpicker.ColorPickerView;
+import com.colorpicker.Utils;
+import com.colorpicker.builder.PaintBuilder;
+public class AlphaSlider extends AbsCustomSlider {
+ public int color;
+ private Paint alphaPatternPaint = PaintBuilder.newPaint().build();
+ private Paint barPaint = PaintBuilder.newPaint().build();
+ private Paint solid = PaintBuilder.newPaint().build();
+ private Paint clearingStroke = PaintBuilder.newPaint().color(0xffffffff).xPerMode(PorterDuff.Mode.CLEAR).build();
+
+ private Paint clearStroke = PaintBuilder.newPaint().build();
+ private Bitmap clearBitmap;
+ private Canvas clearBitmapCanvas;
+
+ private ColorPickerView colorPicker;
+
+ public AlphaSlider(Context context) {
+ super(context);
+ }
+
+ public AlphaSlider(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AlphaSlider(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void createBitmaps() {
+ super.createBitmaps();
+ alphaPatternPaint.setShader(PaintBuilder.createAlphaPatternShader(barHeight * 2));
+ clearBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
+ clearBitmapCanvas = new Canvas(clearBitmap);
+ }
+
+ @Override
+ protected void drawBar(Canvas barCanvas) {
+ int width = barCanvas.getWidth();
+ int height = barCanvas.getHeight();
+
+ barCanvas.drawRect(0, 0, width, height, alphaPatternPaint);
+ int l = Math.max(2, width / 256);
+ for (int x = 0; x <= width; x += l) {
+ float alpha = (float) x / (width - 1);
+ barPaint.setColor(color);
+ barPaint.setAlpha(Math.round(alpha * 255));
+ barCanvas.drawRect(x, 0, x + l, height, barPaint);
+ }
+ }
+
+ @Override
+ protected void onValueChanged(float value) {
+ if (colorPicker != null)
+ colorPicker.setAlphaValue(value);
+ }
+
+ @Override
+ protected void drawHandle(Canvas canvas, float x, float y) {
+ solid.setColor(color);
+ solid.setAlpha(Math.round(value * 255));
+ if (showBorder) canvas.drawCircle(x, y, handleRadius, clearingStroke);
+ if (value < 1) {
+ // this fixes the same artifact issue from ColorPickerView
+ // happens when alpha pattern is drawn underneath a circle with the same size
+ clearBitmapCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ clearBitmapCanvas.drawCircle(x, y, handleRadius * 0.75f + 4, alphaPatternPaint);
+ clearBitmapCanvas.drawCircle(x, y, handleRadius * 0.75f + 4, solid);
+
+ clearStroke = PaintBuilder.newPaint().color(0xffffffff).style(Paint.Style.STROKE).stroke(6).xPerMode(PorterDuff.Mode.CLEAR).build();
+ clearBitmapCanvas.drawCircle(x, y, handleRadius * 0.75f + (clearStroke.getStrokeWidth() / 2), clearStroke);
+ canvas.drawBitmap(clearBitmap, 0, 0, null);
+ } else {
+ canvas.drawCircle(x, y, handleRadius * 0.75f, solid);
+ }
+ }
+
+ public void setColorPicker(ColorPickerView colorPicker) {
+ this.colorPicker = colorPicker;
+ }
+
+ public void setColor(int color) {
+ this.color = color;
+ this.value = Utils.getAlphaPercent(color);
+ if (bar != null) {
+ updateBar();
+ invalidate();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/slider/LightnessSlider.java b/app/src/main/java/com/colorpicker/slider/LightnessSlider.java
new file mode 100644
index 0000000..598a234
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/slider/LightnessSlider.java
@@ -0,0 +1,73 @@
+package com.colorpicker.slider;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.util.AttributeSet;
+
+import com.colorpicker.ColorPickerView;
+import com.colorpicker.Utils;
+import com.colorpicker.builder.PaintBuilder;
+public class LightnessSlider extends AbsCustomSlider {
+ private int color;
+ private Paint barPaint = PaintBuilder.newPaint().build();
+ private Paint solid = PaintBuilder.newPaint().build();
+ private Paint clearingStroke = PaintBuilder.newPaint().color(0xffffffff).xPerMode(PorterDuff.Mode.CLEAR).build();
+
+ private ColorPickerView colorPicker;
+
+ public LightnessSlider(Context context) {
+ super(context);
+ }
+
+ public LightnessSlider(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public LightnessSlider(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void drawBar(Canvas barCanvas) {
+ int width = barCanvas.getWidth();
+ int height = barCanvas.getHeight();
+
+ float[] hsv = new float[3];
+ Color.colorToHSV(color, hsv);
+ int l = Math.max(2, width / 256);
+ for (int x = 0; x <= width; x += l) {
+ hsv[2] = (float) x / (width - 1);
+ barPaint.setColor(Color.HSVToColor(hsv));
+ barCanvas.drawRect(x, 0, x + l, height, barPaint);
+ }
+ }
+
+ @Override
+ protected void onValueChanged(float value) {
+ if (colorPicker != null)
+ colorPicker.setLightness(value);
+ }
+
+ @Override
+ protected void drawHandle(Canvas canvas, float x, float y) {
+ solid.setColor(Utils.colorAtLightness(color, value));
+ if (showBorder) canvas.drawCircle(x, y, handleRadius, clearingStroke);
+ canvas.drawCircle(x, y, handleRadius * 0.75f, solid);
+ }
+
+ public void setColorPicker(ColorPickerView colorPicker) {
+ this.colorPicker = colorPicker;
+ }
+
+ public void setColor(int color) {
+ this.color = color;
+ this.value = Utils.lightnessOfColor(color);
+ if (bar != null) {
+ updateBar();
+ invalidate();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/colorpicker/slider/OnValueChangedListener.java b/app/src/main/java/com/colorpicker/slider/OnValueChangedListener.java
new file mode 100644
index 0000000..eb7e029
--- /dev/null
+++ b/app/src/main/java/com/colorpicker/slider/OnValueChangedListener.java
@@ -0,0 +1,5 @@
+package com.colorpicker.slider;
+
+public interface OnValueChangedListener {
+ void onValueChanged(float value);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dirror/lyricviewx/ILyricViewX.kt b/app/src/main/java/com/dirror/lyricviewx/ILyricViewX.kt
new file mode 100644
index 0000000..d0422ee
--- /dev/null
+++ b/app/src/main/java/com/dirror/lyricviewx/ILyricViewX.kt
@@ -0,0 +1,257 @@
+package com.dirror.lyricviewx
+
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.text.Layout
+import androidx.annotation.ColorInt
+import androidx.annotation.FloatRange
+import androidx.annotation.Px
+import java.io.File
+
+const val GRAVITY_CENTER = 0 // 居中
+const val GRAVITY_LEFT = 1 // 左
+const val GRAVITY_RIGHT = 2 // 右
+
+fun Int.toLayoutAlign(): Layout.Alignment {
+ return when (this) {
+ GRAVITY_LEFT -> Layout.Alignment.ALIGN_NORMAL
+ GRAVITY_CENTER -> Layout.Alignment.ALIGN_CENTER
+ GRAVITY_RIGHT -> Layout.Alignment.ALIGN_OPPOSITE
+ else -> Layout.Alignment.ALIGN_CENTER
+ }
+}
+
+/**
+ * LyricViewX 接口
+ * 从 LyricViewX 提取,方便管理
+ *
+ * @author Moriafly
+ * @since 2021年1月28日16:29:16
+ */
+interface LyricViewXInterface {
+
+ /**
+ * 设置整句之间的间隔高度
+ * @param height px
+ */
+ fun setSentenceDividerHeight(@Px height: Float)
+
+ /**
+ * 设置原句与翻译之间的间隔高度
+ * @param height px
+ */
+ fun setTranslateDividerHeight(@Px height: Float)
+
+ /**
+ * 设置歌词整体的垂直偏移值,配合[setHorizontalOffsetPercent]使用
+ * @param offset px
+ *
+ * @see [setHorizontalOffsetPercent]
+ */
+ fun setHorizontalOffset(@Px offset: Float)
+
+ /**
+ * 设置歌词整体的垂直偏移,相对于控件高度的百分比,0.5f即表示居中,配合[setHorizontalOffset]使用
+ *
+ * @param percent 0.0f ~ 1.0f
+ *
+ * @see [setHorizontalOffset]
+ */
+ fun setHorizontalOffsetPercent(@FloatRange(from = 0.0, to = 1.0) percent: Float)
+
+ /**
+ * 设置翻译相对与原词之间的缩放比例值
+ * @param scaleValue 一般来说 0.8f 是个不错的值
+ */
+ fun setTranslateTextScaleValue(@FloatRange(from = 0.1, to = 2.0) scaleValue: Float)
+
+ /**
+ * 设置文字的对齐方向
+ */
+ fun setTextGravity(gravity: Int)
+
+ /**
+ * 设置非当前行歌词字体颜色 [normalColor]
+ */
+ fun setNormalColor(@ColorInt normalColor: Int)
+
+ /**
+ * 普通歌词文本字体大小 [size],单位 px
+ */
+ fun setNormalTextSize(@Px size: Float)
+
+ /**
+ * 当前歌词文本字体大小
+ */
+ fun setCurrentTextSize(size: Float)
+
+ /**
+ * 设置当前行歌词的字体颜色
+ */
+ fun setCurrentColor(currentColor: Int)
+
+ /**
+ * 设置拖动歌词时选中歌词的字体颜色
+ */
+ fun setTimelineTextColor(timelineTextColor: Int)
+
+ /**
+ * 设置拖动歌词时时间线的颜色
+ */
+ fun setTimelineColor(timelineColor: Int)
+
+ /**
+ * 设置拖动歌词时右侧时间字体颜色
+ */
+ fun setTimeTextColor(timeTextColor: Int)
+
+ /**
+ * 设置歌词为空时屏幕中央显示的文字 [label],如“暂无歌词”
+ */
+ fun setLabel(label: String)
+
+ /**
+ * 加载歌词文本
+ * 两种语言的歌词时间戳需要一致
+ *
+ * @param mainLyricText 第一种语言歌词文本
+ * @param secondLyricText 可选,第二种语言歌词文本
+ */
+ fun loadLyric(mainLyricText: String?, secondLyricText: String? = null)
+
+ /**
+ * 加载歌词 [LyricEntry] 集合
+ * 如果你在 Service 等地方自行解析歌词包装成 [LyricEntry] 集合,那么可以使用此方法载入歌词
+ *
+ * @param lyricEntries 歌词集合
+ * @since 1.3.1
+ */
+ fun loadLyric(lyricEntries: List)
+
+ /**
+ * 刷新歌词
+ *
+ * @param time 当前播放时间
+ */
+ fun updateTime(time: Long, force: Boolean = false)
+
+ /**
+ * 设置歌词是否允许拖动
+ *
+ * @param draggable 是否允许拖动
+ * @param onPlayClickListener 设置歌词拖动后播放按钮点击监听器,如果允许拖动,则不能为 null
+ */
+ fun setDraggable(draggable: Boolean, onPlayClickListener: OnPlayClickListener?)
+
+ /**
+ * 设置单击
+ */
+ fun setOnSingerClickListener(onSingerClickListener: OnSingleClickListener?)
+
+ /**
+ * 获取当前歌词每句实体,可用于歌词分享
+ *
+ * @return LyricEntry 集合
+ */
+ fun getLyricEntryList(): List
+
+ /**
+ * 设置当前歌词每句实体
+ */
+ fun setLyricEntryList(newList: List)
+
+ /**
+ * 获取当前行歌词
+ */
+ fun getCurrentLineLyricEntry(): LyricEntry?
+
+ /**
+ * 为歌词设置自定义的字体
+ *
+ * @param file 字体文件
+ */
+ fun setLyricTypeface(file: File)
+
+ /**
+ * 为歌词设置自定义的字体
+ *
+ * @param path 字体文件路径
+ */
+ fun setLyricTypeface(path: String)
+
+ /**
+ * 为歌词设置自定义的字体,可为空,若为空则应清除字体
+ *
+ * @param typeface 字体对象
+ */
+ fun setLyricTypeface(typeface: Typeface?)
+
+ /**
+ * 为歌词的过渡动画设置阻尼比(数值越大,回弹次数越多)
+ *
+ * @param dampingRatio 阻尼比 详见[androidx.dynamicanimation.animation.SpringForce]
+ */
+ fun setDampingRatioForLyric(dampingRatio: Float)
+
+ /**
+ * 为歌词视图的滚动动画设置阻尼比(数值越大,回弹次数越多)
+ *
+ * @param dampingRatio 阻尼比 详见[androidx.dynamicanimation.animation.SpringForce]
+ */
+ fun setDampingRatioForViewPort(dampingRatio: Float)
+
+ /**
+ * 为歌词的过渡动画设置刚度(数值越大,动画越短)
+ *
+ * @param stiffness 刚度 详见[androidx.dynamicanimation.animation.SpringForce]
+ */
+ fun setStiffnessForLyric(stiffness: Float)
+
+ /**
+ * 为歌词视图的滚动动画设置刚度(数值越大,动画越短)
+ *
+ * @param stiffness 刚度 详见[androidx.dynamicanimation.animation.SpringForce]
+ */
+ fun setStiffnessForViewPort(stiffness: Float)
+
+ /**
+ * 设置跳转播放按钮
+ */
+ fun setPlayDrawable(drawable: Drawable)
+
+ /**
+ * 设置是否绘制歌词翻译
+ */
+ fun setIsDrawTranslation(isDrawTranslation: Boolean)
+
+ /**
+ * 是否开启特定的模糊效果
+ */
+ fun setIsEnableBlurEffect(isEnableBlurEffect: Boolean)
+
+ /**
+ * 设置元素的偏移百分比,0.5f即表示居中
+ *
+ * @param itemOffsetPercent 0f ~ 1f 偏移百分比
+ */
+ fun setItemOffsetPercent(@FloatRange(from = 0.0, to = 1.0) itemOffsetPercent: Float)
+}
+
+/**
+ * 播放按钮点击监听器,点击后应该跳转到指定播放位置
+ */
+interface OnPlayClickListener {
+ /**
+ * 播放按钮被点击,应该跳转到指定播放位置
+ *
+ * @return 是否成功消费该事件,如果成功消费,则会更新UI
+ */
+ fun onPlayClick(time: Long): Boolean
+}
+
+/**
+ * 点击歌词布局
+ */
+interface OnSingleClickListener {
+ fun onClick()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dirror/lyricviewx/LyricEntry.kt b/app/src/main/java/com/dirror/lyricviewx/LyricEntry.kt
new file mode 100644
index 0000000..b92499d
--- /dev/null
+++ b/app/src/main/java/com/dirror/lyricviewx/LyricEntry.kt
@@ -0,0 +1,82 @@
+package com.dirror.lyricviewx
+
+import android.os.Build
+import android.text.Layout
+import android.text.StaticLayout
+import android.text.TextPaint
+
+/**
+ * 一行歌词实体
+ * @since 2021年1月19日09:51:40 Moriafly 基于 LrcEntry 改造,转换为 kt ,移除部分过时方法
+ * @param time 歌词时间
+ * @param text 歌词文本
+ */
+class LyricEntry(@JvmField val time: Long, @JvmField val text: String) : Comparable {
+
+ /**
+ * 第二文本
+ */
+ @JvmField
+ var secondText: String? = null
+
+ /**
+ * staticLayout
+ */
+ var staticLayout: StaticLayout? = null
+ private set
+ var secondStaticLayout: StaticLayout? = null
+ private set
+
+ @Deprecated("存在不显示翻译的情况,会导致offset发生改变,故不再固定存储offset")
+ /**
+ * 歌词距离视图顶部的距离
+ */
+ var offset = Float.MIN_VALUE
+
+ /**
+ * 初始化
+ * @param textPaint 文本画笔
+ * @param width 宽度
+ * @param align 位置
+ */
+ fun init(
+ textPaint: TextPaint,
+ secondTextPaint: TextPaint,
+ width: Int, align: Layout.Alignment
+ ) {
+ staticLayout = createStaticLayout(text, textPaint, width, align)
+ secondStaticLayout = createStaticLayout(secondText, secondTextPaint, width, align)
+ offset = Float.MIN_VALUE
+ }
+
+ /**
+ * 继承 Comparable 比较
+ * @param other LyricEntry
+ * @return 时间差
+ */
+ override fun compareTo(other: LyricEntry): Int {
+ return (time - other.time).toInt()
+ }
+
+ companion object {
+ fun createStaticLayout(
+ text: String?,
+ paint: TextPaint,
+ width: Number,
+ align: Layout.Alignment
+ ): StaticLayout? {
+ if (text.isNullOrEmpty()) return null
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ StaticLayout.Builder
+ .obtain(text, 0, text.length, paint, width.toInt())
+ .setAlignment(align)
+ .setLineSpacing(0f, 1f)
+ .setIncludePad(false)
+ .build()
+ } else {
+ StaticLayout(text, paint, width.toInt(), align, 1f, 0f, false)
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dirror/lyricviewx/LyricUtil.kt b/app/src/main/java/com/dirror/lyricviewx/LyricUtil.kt
new file mode 100644
index 0000000..cb3b4b4
--- /dev/null
+++ b/app/src/main/java/com/dirror/lyricviewx/LyricUtil.kt
@@ -0,0 +1,289 @@
+package com.dirror.lyricviewx
+
+import android.animation.ArgbEvaluator
+import android.animation.ValueAnimator
+import android.annotation.SuppressLint
+import android.graphics.Rect
+import android.text.TextUtils
+import android.text.format.DateUtils
+import android.view.MotionEvent
+import java.io.*
+import java.net.HttpURLConnection
+import java.net.URL
+import java.nio.charset.StandardCharsets
+import java.util.*
+import java.util.regex.Pattern
+
+/**
+ * 工具类
+ * 原 LrcUtils 转 Kotlin
+ */
+object LyricUtil {
+
+ private val PATTERN_LINE = Pattern.compile("((\\[\\d\\d:\\d\\d\\.\\d{2,3}])+)(.+)")
+ private val PATTERN_TIME = Pattern.compile("\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})]")
+ private val argbEvaluator = ArgbEvaluator()
+
+ /**
+ * 从文件解析双语歌词
+ */
+ fun parseLrc(lrcFiles: Array?): List? {
+ if (lrcFiles == null || lrcFiles.size != 2 || lrcFiles[0] == null) {
+ return null
+ }
+ val mainLrcFile = lrcFiles[0]
+ val secondLrcFile = lrcFiles[1]
+ val mainEntryList = parseLrc(mainLrcFile)
+ val secondEntryList = parseLrc(secondLrcFile)
+ if (mainEntryList != null && secondEntryList != null) {
+ for (mainEntry in mainEntryList) {
+ for (secondEntry in secondEntryList) {
+ if (mainEntry.time == secondEntry.time) {
+ mainEntry.secondText = secondEntry.text
+ }
+ }
+ }
+ }
+ return mainEntryList
+ }
+
+ /**
+ * 从文件解析歌词
+ */
+ private fun parseLrc(lrcFile: File?): List? {
+ if (lrcFile == null || !lrcFile.exists()) {
+ return null
+ }
+ val entryList: MutableList = ArrayList()
+ try {
+ val br =
+ BufferedReader(InputStreamReader(FileInputStream(lrcFile), StandardCharsets.UTF_8))
+ var line: String
+ while (br.readLine().also { line = it } != null) {
+ val list = parseLine(line)
+ if (list != null && list.isNotEmpty()) {
+ entryList.addAll(list)
+ }
+ }
+ br.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ entryList.sort()
+ return entryList
+ }
+
+ /**
+ * 从文本解析双语歌词
+ */
+ fun parseLrc(lrcTexts: Array?): List? {
+ if (lrcTexts == null || lrcTexts.size != 2 || TextUtils.isEmpty(lrcTexts[0])) {
+ return null
+ }
+ val mainLrcText = lrcTexts[0]
+ val secondLrcText = lrcTexts[1]
+ val mainEntryList = mainLrcText?.let { parseLrc(it) }
+
+ /**
+ * 当输入的secondLrcText为空时,按如下格式解析歌词
+ * (音乐标签下载的第二种歌词格式)
+ *
+ * [00:21.11]いつも待ち合わせより15分前集合
+ * [00:21.11]总会比相约时间早15分钟集合
+ * [00:28.32]駅の改札ぬける
+ * [00:28.32]穿过车站的检票口
+ * [00:31.39]ざわめきにわくわくだね
+ * [00:31.39]嘈杂声令内心兴奋不已
+ * [00:35.23]どこへ向かうかなんて
+ * [00:35.23]不在意接下来要去哪里
+ */
+ if (TextUtils.isEmpty(secondLrcText)) {
+ var lastEntry: LyricEntry? = null
+ return mainEntryList?.filter { now ->
+ if (lastEntry == null) {
+ lastEntry = now
+ return@filter true
+ }
+
+ if (lastEntry!!.time == now.time) {
+ lastEntry!!.secondText = now.text
+ lastEntry = null
+ return@filter false
+ }
+
+ lastEntry = now
+ true
+ }
+ }
+
+ val secondEntryList = secondLrcText?.let { parseLrc(it) }
+ if (mainEntryList != null && secondEntryList != null) {
+ for (mainEntry in mainEntryList) {
+ for (secondEntry in secondEntryList) {
+ if (mainEntry.time == secondEntry.time) {
+ mainEntry.secondText = secondEntry.text
+ }
+ }
+ }
+ }
+ return mainEntryList
+ }
+
+ /**
+ * 从文本解析歌词
+ */
+ private fun parseLrc(lrcText: String): List? {
+ var lyricText = lrcText.trim()
+ if (TextUtils.isEmpty(lyricText)) return null
+
+ if (lyricText.startsWith("\uFEFF")) {
+ lyricText = lyricText.replace("\uFEFF", "")
+ }
+
+ // 针对传入 Language="Media Monkey Format"; Lyrics="......"; 的情况
+ lyricText = lyricText.substringAfter("Lyrics=\"")
+ .substringBeforeLast("\";")
+
+ val entryList: MutableList = ArrayList()
+ val array = lyricText.split("\\n".toRegex()).toTypedArray()
+ for (line in array) {
+ val list = parseLine(line)
+ if (!list.isNullOrEmpty()) {
+ entryList.addAll(list)
+ }
+ }
+ entryList.sort()
+ return entryList
+ }
+
+ /**
+ * 获取网络文本,需要在工作线程中执行
+ */
+ fun getContentFromNetwork(url: String?, charset: String?): String? {
+ var lrcText: String? = null
+ try {
+ val url = URL(url)
+ val conn = url.openConnection() as HttpURLConnection
+ conn.requestMethod = "GET"
+ conn.connectTimeout = 10000
+ conn.readTimeout = 10000
+ if (conn.responseCode == 200) {
+ val `is` = conn.inputStream
+ val bos = ByteArrayOutputStream()
+ val buffer = ByteArray(1024)
+ var len: Int
+ while (`is`.read(buffer).also { len = it } != -1) {
+ bos.write(buffer, 0, len)
+ }
+ `is`.close()
+ bos.close()
+ lrcText = bos.toString(charset)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return lrcText
+ }
+
+ /**
+ * 解析一行歌词
+ */
+ private fun parseLine(line: String): List? {
+ var lyricLine = line
+ if (TextUtils.isEmpty(lyricLine)) {
+ return null
+ }
+ lyricLine = lyricLine.trim { it <= ' ' }
+ // [00:17.65]让我掉下眼泪的
+ val lineMatcher = PATTERN_LINE.matcher(lyricLine)
+ if (!lineMatcher.matches()) {
+ return null
+ }
+ val times = lineMatcher.group(1)!!
+ val text = lineMatcher.group(3)!!
+ val entryList: MutableList = ArrayList()
+
+ // [00:17.65]
+ val timeMatcher = PATTERN_TIME.matcher(times)
+ while (timeMatcher.find()) {
+ val min = timeMatcher.group(1)!!.toLong()
+ val sec = timeMatcher.group(2)!!.toLong()
+ val milString = timeMatcher.group(3)!!
+ var mil = milString.toLong()
+ // 如果毫秒是两位数,需要乘以 10,when 新增支持 1 - 6 位毫秒,很多获取的歌词存在不同的毫秒位数
+ when (milString.length) {
+ 1 -> mil *= 100
+ 2 -> mil *= 10
+ 4 -> mil /= 10
+ 5 -> mil /= 100
+ 6 -> mil /= 1000
+ }
+ val time = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil
+ entryList.add(LyricEntry(time, text))
+ }
+ return entryList
+ }
+
+ /**
+ * 转为[分:秒]
+ */
+ fun formatTime(milli: Long): String {
+ val m = (milli / DateUtils.MINUTE_IN_MILLIS).toInt()
+ val s = (milli / DateUtils.SECOND_IN_MILLIS % 60).toInt()
+ val mm = String.format(Locale.getDefault(), "%02d", m)
+ val ss = String.format(Locale.getDefault(), "%02d", s)
+ return "$mm:$ss"
+ }
+
+ /**
+ * BUG java.lang.NoSuchFieldException: No field sDurationScale in class Landroid/animation/ValueAnimator; #3
+ */
+ @SuppressLint("SoonBlockedPrivateApi")
+ @Deprecated("")
+ fun resetDurationScale() {
+ try {
+ val mField = ValueAnimator::class.java.getDeclaredField("sDurationScale")
+ mField.isAccessible = true
+ mField.setFloat(null, 1f)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * 结合fraction,计算两个值之间的比例
+ */
+ fun calcScaleValue(a: Float, b: Float, f: Float, reverse: Boolean = false): Float {
+ if (b == 0f) return 1f
+ return 1f + ((a - b) / b) * (if (reverse) 1f - f else f)
+ }
+
+ /**
+ * 颜色值插值函数
+ */
+ fun lerpColor(a: Int, b: Int, f: Float): Int {
+ return argbEvaluator.evaluate(f, a, b) as Int
+ }
+
+ /**
+ * 简单的插值函数
+ */
+ fun lerp(from: Float, to: Float, fraction: Float): Float {
+ return from + (to - from) * fraction
+ }
+
+ /**
+ * 判断MotionEvent是否发生在Rect中
+ */
+ fun MotionEvent.insideOf(rect: Rect?): Boolean {
+ rect ?: return false
+ return rect.contains(x.toInt(), y.toInt())
+ }
+
+ fun normalize(min: Float, max: Float, value: Float, limit: Boolean = false): Float {
+ if (min == max) return 1f
+ return ((value - min) / (max - min)).let {
+ if (limit) it.coerceIn(0f, 1f) else it
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dirror/lyricviewx/LyricViewX.kt b/app/src/main/java/com/dirror/lyricviewx/LyricViewX.kt
new file mode 100644
index 0000000..d967af5
--- /dev/null
+++ b/app/src/main/java/com/dirror/lyricviewx/LyricViewX.kt
@@ -0,0 +1,1098 @@
+package com.dirror.lyricviewx
+import android.animation.ValueAnimator
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.os.Looper
+import android.text.Layout
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.text.format.DateUtils
+import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.GestureDetector.SimpleOnGestureListener
+import android.view.MotionEvent
+import android.widget.Scroller
+import androidx.annotation.FloatRange
+import androidx.core.content.ContextCompat
+import androidx.dynamicanimation.animation.SpringForce
+import androidx.dynamicanimation.animation.springAnimationOf
+import androidx.dynamicanimation.animation.withSpringForceProperties
+import com.dirror.lyricviewx.LyricUtil.calcScaleValue
+import com.dirror.lyricviewx.LyricUtil.formatTime
+import com.dirror.lyricviewx.LyricUtil.insideOf
+import com.dirror.lyricviewx.LyricUtil.lerp
+import com.dirror.lyricviewx.LyricUtil.lerpColor
+import com.dirror.lyricviewx.LyricUtil.normalize
+import com.dirror.lyricviewx.extension.BlurMaskFilterExt
+import com.lalilu.easeview.EaseView
+import com.lalilu.easeview.animatevalue.BoolValue
+import com.lalilu.easeview.animatevalue.FloatListAnimateValue
+import com.muqingbfq.R
+import java.io.File
+import kotlin.concurrent.thread
+import kotlin.math.abs
+import kotlin.math.max
+open class LyricViewX : EaseView, LyricViewXInterface {
+ constructor(context: Context) : super(context) {
+ init(null)
+ }
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
+ init(attrs)
+ }
+
+ companion object {
+ private const val TAG = "LyricViewX"
+ // 时间线持续时间
+ private const val TIMELINE_KEEP_TIME = 3 * DateUtils.SECOND_IN_MILLIS
+
+ /** 单句歌词集合 */
+ @JvmField
+ val lyricEntryList: MutableList = ArrayList()
+
+ @JvmStatic
+ fun lrc(a: String?, b: String?) {
+ lyricEntryList.clear()
+ val lrcEntries = LyricUtil.parseLrc(arrayOf(a, b))
+ if (!lrcEntries.isNullOrEmpty()) {
+ lyricEntryList.addAll(lrcEntries)
+ }
+ lyricEntryList.sort()
+ }
+ }
+
+ private val readyHelper = ReadyHelper()
+ private val blurMaskFilterExt = BlurMaskFilterExt()
+
+
+ /** 主歌词画笔 */
+ private val lyricPaint = TextPaint()
+
+ /** 副歌词(一般为翻译歌词)画笔 */
+ private val secondLyricPaint = TextPaint()
+
+ /** 时间文字画笔 */
+ private val timePaint = TextPaint()
+
+ private var timeFontMetrics: Paint.FontMetrics? = null
+
+ /** 跳转播放按钮 */
+ private var playDrawable: Drawable? = null
+
+ private var translateDividerHeight = 0f
+ private var sentenceDividerHeight = 0f
+ private var animationDuration: Long = 0
+ private var normalTextColor = 0
+ private var normalTextSize = 0f
+ private var currentTextColor = 0
+ private var currentTextSize = 0f
+ private var translateTextScaleValue = 1f
+ private var timelineTextColor = 0
+ private var timelineColor = 0
+ private var timeTextColor = 0
+ private var drawableWidth = 0
+ private var timeTextWidth = 0
+ private var defaultLabel: String? = null
+ private var lrcPadding = 0f
+ private var onPlayClickListener: OnPlayClickListener? = null
+ private var onSingerClickListener: OnSingleClickListener? = null
+ private var animator: ValueAnimator? = null
+ private var gestureDetector: GestureDetector? = null
+ private var scroller: Scroller? = null
+ private var flag: Any? = null
+ private var isTouching = false
+ private var isFling = false
+ private var textGravity = GRAVITY_CENTER // 歌词显示位置,靠左 / 居中 / 靠右
+ private var horizontalOffset: Float = 0f
+ private var horizontalOffsetPercent: Float = 0.5f
+ private var itemOffsetPercent: Float = 0.5f
+ private var dampingRatioForLyric: Float = SpringForce.DAMPING_RATIO_LOW_BOUNCY
+ private var dampingRatioForViewPort: Float = SpringForce.DAMPING_RATIO_NO_BOUNCY
+ private var stiffnessForLyric: Float = SpringForce.STIFFNESS_LOW
+ private var stiffnessForViewPort: Float = SpringForce.STIFFNESS_VERY_LOW
+
+ private var currentLine = 0 // 当前高亮显示的歌词
+ private val focusLine: Int // 当前焦点歌词
+ get() = if (isTouching || isFling) centerLine else currentLine
+
+ /**
+ * 获取当前在视图中央的行数
+ */
+ private val centerLine: Int
+ get() {
+ var centerLine = 0
+ var minDistance = Float.MAX_VALUE
+ var tempDistance: Float
+
+ for (i in lyricEntryList.indices) {
+ tempDistance = abs(mViewPortOffset - getOffset(i))
+ if (tempDistance < minDistance) {
+ minDistance = tempDistance
+ centerLine = i
+ }
+ }
+ return centerLine
+ }
+
+ /**
+ * 获取歌词宽度
+ */
+ open val lrcWidth: Float
+ get() = width - lrcPadding * 2
+
+ /**
+ * 歌词整体的垂直偏移值
+ */
+ open val startOffset: Float
+ get() = height.toFloat() * horizontalOffsetPercent + horizontalOffset
+
+
+ /**
+ * 原有的mOffset被拆分成两个独立的offset,这样可以更好地让进度和拖拽滚动独立开来
+ */
+ private var mCurrentOffset = 0f // 实际的歌词进度Offset
+ private var mViewPortOffset = 0f // 歌词显示窗口的Offset
+
+ private var animateProgress = 0f // 动画进度
+ private var animateTargetOffset = 0f // 动画目标Offset
+ private var animateStartOffset = 0f // 动画起始Offset
+
+ private val viewPortSpringAnimator = springAnimationOf(
+ getter = { mViewPortOffset },
+ setter = { value ->
+ if (!isShowTimeline.value && !isTouching && !isFling) {
+ mViewPortOffset = value
+ invalidate()
+ }
+ }
+ ).withSpringForceProperties {
+ dampingRatio = dampingRatioForViewPort
+ stiffness = stiffnessForViewPort
+ finalPosition = 0f
+ }
+
+ /**
+ * 弹性动画Scroller
+ */
+ private val progressSpringAnimator = springAnimationOf(
+ getter = { mCurrentOffset },
+ setter = { value ->
+ animateProgress = normalize(animateStartOffset, animateTargetOffset, value)
+ mCurrentOffset = value
+
+ if (!isShowTimeline.value && !isTouching && !isFling) {
+ viewPortSpringAnimator.animateToFinalPosition(animateTargetOffset)
+ }
+ invalidate()
+ }
+ ).withSpringForceProperties {
+ dampingRatio = dampingRatioForLyric
+ stiffness = stiffnessForLyric
+ finalPosition = 0f
+ }
+
+ @SuppressLint("CustomViewStyleable")
+ private fun init(attrs: AttributeSet?) {
+ readyHelper.readyState = STATE_INITIALIZING
+ val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LyricView)
+ currentTextSize = typedArray.getDimension(R.styleable.LyricView_lrcTextSize, resources.getDimension(R.dimen.lrc_text_size))
+ normalTextSize = typedArray.getDimension(R.styleable.LyricView_lrcNormalTextSize, resources.getDimension(R.dimen.lrc_text_size))
+ if (normalTextSize == 0f) {
+ normalTextSize = currentTextSize
+ }
+
+ sentenceDividerHeight =
+ typedArray.getDimension(R.styleable.LyricView_lrcSentenceDividerHeight, resources.getDimension(R.dimen.lrc_sentence_divider_height))
+ translateDividerHeight =
+ typedArray.getDimension(R.styleable.LyricView_lrcTranslateDividerHeight, resources.getDimension(R.dimen.lrc_translate_divider_height))
+ val defDuration = resources.getInteger(R.integer.lrc_animation_duration)
+ animationDuration = typedArray.getInt(R.styleable.LyricView_lrcAnimationDuration, defDuration).toLong()
+ animationDuration =
+ if (animationDuration < 0) defDuration.toLong() else animationDuration
+
+ normalTextColor = typedArray.getColor(
+ R.styleable.LyricView_lrcNormalTextColor,
+ ContextCompat.getColor(context, R.color.lrc_normal_text_color)
+ )
+ currentTextColor = typedArray.getColor(
+ R.styleable.LyricView_lrcCurrentTextColor,
+ ContextCompat.getColor(context, R.color.lrc_current_text_color)
+ )
+ timelineTextColor = typedArray.getColor(
+ R.styleable.LyricView_lrcTimelineTextColor,
+ ContextCompat.getColor(context, R.color.lrc_timeline_text_color)
+ )
+ defaultLabel = typedArray.getString(R.styleable.LyricView_lrcLabel)
+ defaultLabel = if (defaultLabel.isNullOrEmpty()) "暂无歌词" else defaultLabel
+ lrcPadding = typedArray.getDimension(R.styleable.LyricView_lrcPadding, 0f)
+ timelineColor = typedArray.getColor(
+ R.styleable.LyricView_lrcTimelineColor,
+ ContextCompat.getColor(context, R.color.lrc_timeline_color)
+ )
+ val timelineHeight = typedArray.getDimension(
+ R.styleable.LyricView_lrcTimelineHeight,
+ resources.getDimension(R.dimen.lrc_timeline_height)
+ )
+ playDrawable = typedArray.getDrawable(R.styleable.LyricView_lrcPlayDrawable)
+ playDrawable = if (playDrawable == null) ContextCompat.getDrawable(
+ context,
+ R.drawable.lrc_play
+ ) else playDrawable
+ timeTextColor = typedArray.getColor(
+ R.styleable.LyricView_lrcTimeTextColor,
+ ContextCompat.getColor(context, R.color.lrc_time_text_color)
+ )
+ val timeTextSize = typedArray.getDimension(
+ R.styleable.LyricView_lrcTimeTextSize,
+ resources.getDimension(R.dimen.lrc_time_text_size)
+ )
+ textGravity = typedArray.getInteger(R.styleable.LyricView_lrcTextGravity, GRAVITY_CENTER)
+ translateTextScaleValue = typedArray.getFloat(R.styleable.LyricView_lrcTranslateTextScaleValue, 1f)
+ horizontalOffset = typedArray.getDimension(R.styleable.LyricView_lrcHorizontalOffset, 0f)
+ horizontalOffsetPercent = typedArray.getDimension(R.styleable.LyricView_lrcHorizontalOffsetPercent, 0.5f)
+ itemOffsetPercent = typedArray.getDimension(R.styleable.LyricView_lrcItemOffsetPercent, 0.5f)
+ isDrawTranslation = typedArray.getBoolean(R.styleable.LyricView_lrcIsDrawTranslation, false)
+ typedArray.recycle()
+ drawableWidth = resources.getDimension(R.dimen.lrc_drawable_width).toInt()
+ timeTextWidth = resources.getDimension(R.dimen.lrc_time_width).toInt()
+ lyricPaint.isAntiAlias = true
+ lyricPaint.textSize = currentTextSize
+ lyricPaint.textAlign = Paint.Align.LEFT
+// lyricPaint.setShadowLayer(0.1f, 0f, 1f, Color.DKGRAY)
+ secondLyricPaint.isAntiAlias = true
+ secondLyricPaint.textSize = currentTextSize
+ secondLyricPaint.textAlign = Paint.Align.LEFT
+// secondLyricPaint.setShadowLayer(0.1f, 0f, 1f, Color.DKGRAY)
+ timePaint.isAntiAlias = true
+ timePaint.textSize = timeTextSize
+ timePaint.textAlign = Paint.Align.CENTER
+ timePaint.strokeWidth = timelineHeight
+ timePaint.strokeCap = Paint.Cap.ROUND
+ timeFontMetrics = timePaint.fontMetrics
+ gestureDetector = GestureDetector(context, mSimpleOnGestureListener)
+ gestureDetector!!.setIsLongpressEnabled(false)
+ scroller = Scroller(context)
+ }
+
+ /**
+ * 歌词是否有效
+ * @return true,如果歌词有效,否则false
+ */
+ private fun hasLrc(): Boolean {
+ return lyricEntryList.isNotEmpty()
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ if (changed) {
+ initPlayDrawable()
+ initEntryList()
+ if (hasLrc()) {
+ smoothScrollTo(currentLine)
+ }
+ }
+ readyHelper.readyState = STATE_INITIALIZED
+ }
+
+ private val isShowTimeline = BoolValue().also(::registerValue)
+ private val isEnableBlurEffect = BoolValue().also(::registerValue)
+ private val progressKeeper = FloatListAnimateValue().also(::registerValue)
+ private val blurProgressKeeper = FloatListAnimateValue().also(::registerValue)
+
+ private val heightKeeper = LinkedHashMap()
+ private val offsetKeeper = LinkedHashMap()
+ private val minOffsetKeeper = LinkedHashMap()
+ private val maxOffsetKeeper = LinkedHashMap()
+
+ private var viewPortStartOffset: Float = 0f
+ private var isDrawTranslationValue = 0f
+ private var isDrawTranslation: Boolean = false
+ set(value) {
+ if (field == value) return
+ field = value
+ viewPortStartOffset = mViewPortOffset
+ isDrawTranslationAnimator.animateToFinalPosition(if (value) 1000f else 0f)
+ }
+ private val isDrawTranslationAnimator = springAnimationOf(
+ getter = { isDrawTranslationValue * 1000f },
+ setter = {
+ isDrawTranslationValue = it / 1000f
+
+ if (!isTouching && !isFling) {
+ viewPortSpringAnimator.cancel()
+
+ val targetOffset = if (isDrawTranslation) getMaxOffset(focusLine) else getMinOffset(focusLine)
+ val animateValue = if (isDrawTranslation) isDrawTranslationValue else 1f - isDrawTranslationValue
+
+ mViewPortOffset = lerp(viewPortStartOffset, targetOffset, animateValue)
+ }
+ invalidate()
+ },
+ ).withSpringForceProperties {
+ dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
+ stiffness = SpringForce.STIFFNESS_LOW
+ finalPosition = if (isDrawTranslation) 1000f else 0f
+ }
+
+ override fun onPreDraw(canvas: Canvas): Boolean {
+ // 无歌词,只渲染一句无歌词的提示语句
+ if (!hasLrc()) {
+ lyricPaint.color = currentTextColor
+ lyricPaint.textSize = normalTextSize
+ LyricEntry.createStaticLayout(
+ defaultLabel,
+ lyricPaint,
+ lrcWidth,
+ Layout.Alignment.ALIGN_CENTER
+ )?.let {
+ drawText(
+ canvas = canvas,
+ staticLayout = it,
+ calcHeightOnly = false,
+ yOffset = startOffset,
+ yClipPercentage = 1f
+ )
+ }
+ return false
+ }
+ return super.onPreDraw(canvas)
+ }
+
+ override fun onDoDraw(canvas: Canvas): Boolean {
+ val centerY = startOffset
+ val currentCenterLine = centerLine
+
+ // 当显示时间线时,需要绘制时间线
+ if (isShowTimeline.value || isShowTimeline.animateValue > 0f) {
+ val alpha = (isShowTimeline.animateValue * 255f).toInt()
+
+ // 绘制播放按钮
+ playDrawable?.let {
+ it.alpha = alpha
+ it.draw(canvas)
+ }
+
+ // 绘制时间线
+ timePaint.color = timelineColor
+ timePaint.alpha = alpha
+ canvas.drawLine(
+ timeTextWidth.toFloat(), centerY,
+ (width - timeTextWidth).toFloat(), centerY, timePaint
+ )
+
+ // 绘制当前时间
+ val timeText = formatTime(lyricEntryList[currentCenterLine].time)
+ val timeX = width - timeTextWidth.toFloat() / 2
+ val timeY = centerY - (timeFontMetrics!!.descent + timeFontMetrics!!.ascent) / 2
+ timePaint.color = timeTextColor
+ timePaint.alpha = alpha
+ canvas.drawText(timeText, timeX, timeY, timePaint)
+ }
+
+ canvas.translate(0f, mViewPortOffset)
+
+ var yOffset = 0f
+ var yMinOffset = 0f
+ var yMaxOffset = 0f
+ var scaleValue: Float
+ var progress: Float
+ var radius: Int
+ var calcHeightOnly: Boolean
+
+ for (i in lyricEntryList.indices) {
+ // 根据上一项所计算得到的offset值,判断当前元素是否在需要绘制的区间,如果不在,则只需要计算高度不进行绘制相关计算
+ calcHeightOnly = getOffset(i - 1) !in (mViewPortOffset - height)..(mViewPortOffset + height)
+ progressKeeper.updateTargetValue(i, if (currentLine == i) animateProgress else 0f)
+ progress = progressKeeper.getValueByIndex(i)
+ scaleValue = 1f
+ radius = 0
+
+ if (!calcHeightOnly) {
+ when {
+ // 当前行动画未结束
+ progress > 0f -> {
+ scaleValue = calcScaleValue(currentTextSize, normalTextSize, progress)
+ lyricPaint.color = lerpColor(normalTextColor, currentTextColor, progress.coerceIn(0f, 1f))
+ }
+
+ isShowTimeline.value && i == currentCenterLine -> {
+ lyricPaint.color = timelineTextColor
+ }
+
+ else -> {
+ lyricPaint.color = normalTextColor
+ }
+ }
+ lyricPaint.textSize = normalTextSize
+ secondLyricPaint.textSize = lyricPaint.textSize * translateTextScaleValue
+ secondLyricPaint.color = lyricPaint.color
+
+ if (isEnableBlurEffect.value || isEnableBlurEffect.animateValue > 0f) {
+ radius = when (i) {
+ currentCenterLine -> 0
+ currentCenterLine + 1 -> 3
+ currentCenterLine + 2, currentCenterLine - 1 -> 7
+ currentCenterLine + 3, currentCenterLine - 2 -> 11
+ currentCenterLine + 4, currentCenterLine - 3 -> 20
+ else -> 20
+ }
+ blurProgressKeeper.updateTargetValue(i, radius.toFloat())
+ radius = blurProgressKeeper.getValueByIndex(i).toInt()
+ radius = (radius * isEnableBlurEffect.animateValue).toInt()
+ }
+ }
+
+ val itemHeight = drawLyricEntry(
+ canvas = canvas,
+ entry = lyricEntryList[i],
+ calcHeightOnly = calcHeightOnly,
+ yOffset = yOffset,
+ scaleValue = scaleValue,
+ blurRadius = radius,
+ ) { minHeight, maxHeight ->
+ minOffsetKeeper[i] = yMinOffset + calcOffsetOfItem(minHeight, sentenceDividerHeight)
+ yMinOffset += minHeight
+
+ maxOffsetKeeper[i] = yMaxOffset + calcOffsetOfItem(maxHeight, sentenceDividerHeight)
+ yMaxOffset += maxHeight
+ }
+ heightKeeper[i] = itemHeight
+ offsetKeeper[i] = yOffset + calcOffsetOfItem(itemHeight, sentenceDividerHeight)
+ yOffset += itemHeight
+ }
+ return super.onDoDraw(canvas)
+ }
+
+ /**
+ * 画一组歌词语句
+ *
+ * @param calcHeightOnly 是否只计算高度
+ * @param yOffset 歌词中心 Y 坐标
+ * @param scaleValue 缩放比例
+ * @param blurRadius 模糊半径
+ *
+ * @return 该组歌词的实际绘制高度
+ */
+ private fun drawLyricEntry(
+ canvas: Canvas,
+ entry: LyricEntry,
+ calcHeightOnly: Boolean,
+ yOffset: Float,
+ scaleValue: Float,
+ blurRadius: Int,
+ callback: (minHeight: Float, maxHeight: Float) -> Unit = { _, _ -> }
+ ): Float {
+ var tempHeight = 0f
+ var minTempHeight = 0f
+ var maxTempHeight = 0f
+
+ entry.staticLayout?.let {
+ tempHeight += drawText(
+ canvas = canvas,
+ staticLayout = it,
+ calcHeightOnly = calcHeightOnly,
+ yOffset = yOffset,
+ yClipPercentage = 1f,
+ scale = scaleValue,
+ blurRadius = blurRadius
+ )
+ minTempHeight = tempHeight
+ maxTempHeight = tempHeight
+
+ entry.secondStaticLayout?.let { second ->
+ tempHeight += translateDividerHeight * isDrawTranslationValue
+ maxTempHeight += translateDividerHeight
+
+ tempHeight += drawText(
+ canvas = canvas,
+ staticLayout = second,
+ calcHeightOnly = calcHeightOnly,
+ yOffset = yOffset + tempHeight,
+ yClipPercentage = isDrawTranslationValue,
+ alpha = isDrawTranslationValue,
+ scale = scaleValue,
+ blurRadius = blurRadius
+ ) { _, max ->
+ maxTempHeight += max
+ }
+ }
+ tempHeight += sentenceDividerHeight
+ minTempHeight += sentenceDividerHeight
+ maxTempHeight += sentenceDividerHeight
+ }
+ callback(minTempHeight, maxTempHeight)
+ return tempHeight
+ }
+
+ /**
+ * 画一行歌词
+ *
+ * @param calcHeightOnly 是否只计算高度
+ * @param yOffset 歌词中心 Y 坐标
+ * @param yClipPercentage 垂直裁剪比例
+ * @param scale 缩放比例
+ * @param alpha 透明度
+ * @param blurRadius 模糊半径 实现类似AppleMusic的歌词语句的模糊效果
+ *
+ * @return 实际绘制高度
+ */
+ private fun drawText(
+ canvas: Canvas,
+ staticLayout: StaticLayout,
+ calcHeightOnly: Boolean = false,
+ yOffset: Float,
+ @FloatRange(from = 0.0, to = 1.0)
+ yClipPercentage: Float = 1f,
+ scale: Float = 1f,
+ alpha: Float = 1f,
+ blurRadius: Int = 0,
+ callback: (minHeight: Float, maxHeight: Float) -> Unit = { _, _ -> }
+ ): Float {
+ if (staticLayout.lineCount == 0) {
+ callback(0f, 0f)
+ return 0f
+ }
+ if (calcHeightOnly) {
+ callback(0f, staticLayout.height.toFloat())
+ return staticLayout.height * yClipPercentage
+ }
+ val lineHeight = staticLayout.height.toFloat() / staticLayout.lineCount.toFloat()
+
+ var yTemp = 0f // y轴临时偏移量
+ var pivotYTemp: Float // 缩放中心Y坐标
+ var itemActualHeight: Float // 单行实际绘制高度
+ var actualHeight = 0f // 实际绘制高度
+
+ staticLayout.paint.alpha = (alpha * 255f).toInt()
+ staticLayout.paint.maskFilter = blurMaskFilterExt.get(blurRadius)
+
+ /**
+ * 由于对StaticLayout整个缩放会使其中间的行间距也被缩放(通过TextPaint的textSize缩放则不会),
+ * 导致其真实渲染高度大于StaticLayout的height属性的值,同时也没有其他的接口能实现相同的缩放效果(对TextSize缩放会显得卡卡的)
+ *
+ * 所以通过Canvas的clipRect,来分别对StaticLayout的每一行文字进行缩放和绘制(StaticLayout的各行高度是一致的)
+ */
+ repeat(staticLayout.lineCount) {
+ itemActualHeight = lineHeight * yClipPercentage
+ pivotYTemp = yTemp + itemActualHeight - staticLayout.paint.descent() // TextPaint修改textSize所实现的缩放效果应该就是descent线上的缩放(感觉效果差不多)
+
+ canvas.save()
+ canvas.translate(lrcPadding, yOffset)
+ canvas.clipRect(-lrcPadding, yTemp, staticLayout.width.toFloat() + lrcPadding, yTemp + itemActualHeight)
+
+ // 根据文字的gravity设置缩放基点坐标
+ when (textGravity) {
+ GRAVITY_LEFT -> canvas.scale(scale, scale, 0f, pivotYTemp)
+ GRAVITY_RIGHT -> {
+ canvas.scale(scale, scale, staticLayout.width.toFloat(), pivotYTemp)
+ }
+
+ GRAVITY_CENTER -> {
+ canvas.scale(scale, scale, staticLayout.width / 2f, pivotYTemp)
+ }
+ }
+ staticLayout.draw(canvas)
+ canvas.restore()
+ yTemp += itemActualHeight
+ actualHeight += itemActualHeight
+ }
+ callback(0f, staticLayout.height.toFloat())
+ return actualHeight
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
+ isTouching = false
+ if (hasLrc() && !isFling) {
+ // TODO 应该为Timeline独立设置一个Enable开关, 这样就可以不需要等待Timeline消失
+ postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME)
+ }
+ }
+ return gestureDetector!!.onTouchEvent(event)
+ }
+
+ /**
+ * 手势监听器
+ */
+ private val mSimpleOnGestureListener: SimpleOnGestureListener =
+ object : SimpleOnGestureListener() {
+
+ override fun onDown(e: MotionEvent): Boolean {
+ // 有歌词并且设置了 mOnPlayClickListener
+ if (onPlayClickListener != null) {
+ scroller!!.forceFinished(true)
+ removeCallbacks(hideTimelineRunnable)
+ isTouching = true
+ invalidate()
+ return true
+ }
+ return super.onDown(e)
+ }
+
+ override fun onScroll(
+ e1: MotionEvent,
+ e2: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ if (hasLrc()) {
+ // 如果没显示 Timeline 的时候,distanceY 一段距离后再显示时间线
+ if (!isShowTimeline.value && abs(distanceY) >= 10) {
+ // 滚动显示时间线
+ isShowTimeline.value = true
+ }
+ mViewPortOffset += -distanceY
+ mViewPortOffset.coerceIn(getOffset(lyricEntryList.size - 1), getOffset(0))
+ invalidate()
+ return true
+ }
+ return super.onScroll(e1, e2, distanceX, distanceY)
+ }
+
+ fun onFling(
+ e1: MotionEvent?,
+ e2: MotionEvent?,
+ velocityX: Float?,
+ velocityY: Float?
+ ): Boolean {
+ if (hasLrc()) {
+ scroller!!.fling(
+ 0, mViewPortOffset.toInt(), 0,
+ velocityY!!.toInt(), 0, 0,
+ getOffset(lyricEntryList.size - 1).toInt(),
+ getOffset(0).toInt()
+ )
+ isFling = true
+ return true
+ }
+ return super.onFling(e1!!, e2!!, velocityX!!, velocityY!!)
+ }
+
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+ if (!hasLrc() || !isShowTimeline.value || !e.insideOf(playDrawable?.bounds)) {
+ onSingerClickListener?.onClick()
+ return super.onSingleTapConfirmed(e)
+ }
+
+ val centerLine = centerLine
+ val centerLineTime = lyricEntryList[centerLine].time
+ // onPlayClick 消费了才更新 UI
+ if (onPlayClickListener?.onPlayClick(centerLineTime) == true) {
+ isShowTimeline.value = false
+ removeCallbacks(hideTimelineRunnable)
+ smoothScrollTo(centerLine)
+ invalidate()
+ return true
+ }
+ return super.onSingleTapConfirmed(e)
+ }
+ }
+
+ private val hideTimelineRunnable = Runnable {
+ if (hasLrc() && isShowTimeline.value) {
+ isShowTimeline.value = false
+ smoothScrollTo(currentLine)
+ }
+ }
+
+ override fun computeScroll() {
+ if (scroller!!.computeScrollOffset()) {
+ mViewPortOffset = scroller!!.currY.toFloat()
+ invalidate()
+ }
+ if (isFling && scroller!!.isFinished) {
+ isFling = false
+ if (hasLrc() && !isTouching) {
+ adjustCenter()
+ postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME)
+ }
+ }
+ }
+
+ override fun onDetachedFromWindow() {
+ removeCallbacks(hideTimelineRunnable)
+ super.onDetachedFromWindow()
+ }
+
+ private fun onLrcLoaded(entryList: List?) {
+ if (!entryList.isNullOrEmpty()) {
+ lyricEntryList.addAll(entryList)
+ }
+ lyricEntryList.sort()
+ initEntryList()
+ invalidate()
+ }
+
+ private fun initPlayDrawable() {
+ val l = (timeTextWidth - drawableWidth) / 2
+ val t = startOffset.toInt() - drawableWidth / 2
+ val r = l + drawableWidth
+ val b = t + drawableWidth
+ playDrawable!!.setBounds(l, t, r, b)
+ }
+
+ private fun initEntryList() {
+ if (!hasLrc() || width == 0) {
+ return
+ }
+ /**
+ * StaticLayout 根据初始化时传入的 TextSize 计算换行的位置
+ * 如果 [currentTextSize] 与 [normalTextSize] 相差较大,
+ * 则会导致歌词渲染时溢出边界,或行间距不足挤压在一起
+ *
+ * 故计算出可能的最大 TextSize 以后,用其初始化,使 StaticLayout 拥有足够的高度
+ */
+ lyricPaint.textSize = max(currentTextSize, normalTextSize)
+ secondLyricPaint.textSize = lyricPaint.textSize * translateTextScaleValue
+ for (lrcEntry in lyricEntryList) {
+ lrcEntry.init(
+ lyricPaint, secondLyricPaint,
+ lrcWidth.toInt(), textGravity.toLayoutAlign()
+ )
+ }
+ mCurrentOffset = startOffset
+ mViewPortOffset = startOffset
+ }
+
+ private fun reset() {
+ // TODO 待完善reset的逻辑
+ scroller!!.forceFinished(true)
+ isShowTimeline.value = false
+ isTouching = false
+ isFling = false
+ removeCallbacks(hideTimelineRunnable)
+ lyricEntryList.clear()
+ mCurrentOffset = 0f
+ mViewPortOffset = 0f
+ currentLine = 0
+ invalidate()
+ }
+
+ /**
+ * 将中心行微调至正中心
+ */
+ private fun adjustCenter() {
+ smoothScrollTo(currentLine)
+ }
+
+ /**
+ * 平滑滚动过渡到某一行
+ *
+ * @param line 行号
+ */
+ private fun smoothScrollTo(line: Int) {
+ val offset = getOffset(line)
+ animateStartOffset = mCurrentOffset
+ animateTargetOffset = offset
+ progressSpringAnimator.animateToFinalPosition(offset)
+ }
+
+ /**
+ * 二分法查找当前时间应该显示的行数(最后一个 <= time 的行数)
+ */
+ private fun findShowLine(time: Long): Int {
+ var left = 0
+ var right = lyricEntryList.size
+ while (left <= right) {
+ val middle = (left + right) / 2
+ val middleTime = lyricEntryList[middle].time
+ if (time < middleTime) {
+ right = middle - 1
+ } else {
+ if (middle + 1 >= lyricEntryList.size || time < lyricEntryList[middle + 1].time) {
+ return middle
+ }
+ left = middle + 1
+ }
+ }
+ return 0
+ }
+
+ /**
+ * 计算单个歌词元素的偏移量,用于控制歌词对其中线的位置
+ *
+ * 计算出来的歌词高度包含了分割线的高度,所以需要减去分割线的高度
+ *
+ * @param itemHeight 歌词元素的高度
+ * @param dividerHeight 分割线的高度
+ *
+ * @return 歌词元素的偏移量
+ */
+ protected open fun calcOffsetOfItem(itemHeight: Float, dividerHeight: Float): Float {
+ return (itemHeight - dividerHeight) * itemOffsetPercent
+ }
+
+ /**
+ * 因为添加了 [translateDividerHeight] 用来间隔开歌词与翻译,
+ * 所以直接从 [LyricEntry] 获取高度不可行,
+ * 故使用该 [getLyricHeight] 方法来计算 [LyricEntry] 的高度
+ */
+ @Deprecated("不再单独计算歌词的高度,在绘制时计算并进行更新缓存,所见即所得")
+ open fun getLyricHeight(line: Int): Int {
+ var height = lyricEntryList[line].staticLayout?.height ?: return 0
+ lyricEntryList[line].secondStaticLayout?.height?.let {
+ height += (it + translateDividerHeight).toInt()
+ }
+ return height
+ }
+
+ /**
+ * 获取歌词距离视图顶部的距离
+ */
+ private fun getOffset(line: Int): Float {
+ return startOffset - (offsetKeeper[line] ?: 0f)
+ }
+
+ private fun getMinOffset(line: Int): Float {
+ return startOffset - (minOffsetKeeper[line] ?: 0f)
+ }
+
+ private fun getMaxOffset(line: Int): Float {
+ return startOffset - (maxOffsetKeeper[line] ?: 0f)
+ }
+
+ /**
+ * 在主线程中运行
+ */
+ private fun runOnMain(r: Runnable) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ r.run()
+ } else {
+ post(r)
+ }
+ }
+
+ /**
+ * 以下是公共部分
+ * 用法见接口 [LyricViewXInterface]
+ */
+
+ override fun setSentenceDividerHeight(height: Float) {
+ sentenceDividerHeight = height
+ if (hasLrc()) {
+ smoothScrollTo(currentLine)
+ }
+ postInvalidate()
+ }
+
+ override fun setTranslateDividerHeight(height: Float) {
+ translateDividerHeight = height
+ if (hasLrc()) {
+ smoothScrollTo(currentLine)
+ }
+ postInvalidate()
+ }
+
+ override fun setHorizontalOffset(offset: Float) {
+ horizontalOffset = offset
+ initPlayDrawable()
+ postInvalidate()
+ }
+
+ override fun setHorizontalOffsetPercent(percent: Float) {
+ horizontalOffsetPercent = percent
+ initPlayDrawable()
+ postInvalidate()
+ }
+
+ override fun setTranslateTextScaleValue(scaleValue: Float) {
+ translateTextScaleValue = scaleValue
+ initEntryList()
+ if (hasLrc()) {
+ smoothScrollTo(currentLine)
+ }
+ }
+
+ override fun setTextGravity(gravity: Int) {
+ textGravity = gravity
+ initEntryList()
+ if (hasLrc()) {
+ smoothScrollTo(currentLine)
+ }
+ }
+
+ override fun setNormalColor(normalColor: Int) {
+ normalTextColor = normalColor
+ postInvalidate()
+ }
+
+ override fun setNormalTextSize(size: Float) {
+ normalTextSize = size
+ initEntryList()
+ if (hasLrc()) {
+ smoothScrollTo(currentLine)
+ }
+ }
+
+ override fun setCurrentTextSize(size: Float) {
+ currentTextSize = size
+ initEntryList()
+ if (hasLrc()) {
+ smoothScrollTo(currentLine)
+ }
+ }
+
+ override fun setCurrentColor(currentColor: Int) {
+ currentTextColor = currentColor
+ postInvalidate()
+ }
+
+ override fun setTimelineTextColor(timelineTextColor: Int) {
+ this.timelineTextColor = timelineTextColor
+ postInvalidate()
+ }
+
+ override fun setTimelineColor(timelineColor: Int) {
+ this.timelineColor = timelineColor
+ postInvalidate()
+ }
+
+ override fun setTimeTextColor(timeTextColor: Int) {
+ this.timeTextColor = timeTextColor
+ postInvalidate()
+ }
+
+ override fun setLabel(label: String) {
+ runOnMain {
+ defaultLabel = label
+ this@LyricViewX.invalidate()
+ }
+ }
+
+ override fun loadLyric(mainLyricText: String?, secondLyricText: String?) {
+ runOnMain {
+ reset()
+ val sb = StringBuilder("file://")
+ sb.append(mainLyricText)
+ if (secondLyricText != null) {
+ sb.append("#").append(secondLyricText)
+ }
+ val flag = sb.toString()
+ this@LyricViewX.flag = flag
+ thread {
+ val lrcEntries = LyricUtil.parseLrc(arrayOf(mainLyricText, secondLyricText))
+ runOnMain {
+ onLrcLoaded(lrcEntries)
+ this@LyricViewX.flag = null
+ }
+ }
+ }
+ }
+
+ override fun loadLyric(lyricEntries: List) {
+ runOnMain {
+ reset()
+ onLrcLoaded(lyricEntries)
+ }
+ }
+
+ override fun updateTime(time: Long, force: Boolean) {
+ // 将方法的执行延后至 View 创建完成后执行
+ readyHelper.whenReady {
+ if (!it) return@whenReady
+ if (hasLrc()) {
+ val line = findShowLine(time)
+ if (line != currentLine) {
+ runOnMain {
+ currentLine = line
+ smoothScrollTo(line)
+ }
+ }
+ }
+ }
+ }
+
+ override fun setDraggable(draggable: Boolean, onPlayClickListener: OnPlayClickListener?) {
+ this.onPlayClickListener = if (draggable) {
+ requireNotNull(onPlayClickListener) {
+ "if draggable == true, onPlayClickListener must not be null" }
+ onPlayClickListener
+ } else {
+ null
+ }
+ }
+
+ override fun setOnSingerClickListener(onSingerClickListener: OnSingleClickListener?) {
+ this.onSingerClickListener = onSingerClickListener
+ }
+
+ override fun getLyricEntryList(): List {
+ return lyricEntryList.toList()
+ }
+
+ override fun setLyricEntryList(newList: List) {
+ reset()
+ onLrcLoaded(newList)
+ this@LyricViewX.flag = null
+ }
+
+ override fun getCurrentLineLyricEntry(): LyricEntry? {
+ if (currentLine <= lyricEntryList.lastIndex) {
+ return lyricEntryList[currentLine]
+ }
+ return null
+ }
+
+ override fun setLyricTypeface(file: File) {
+ val typeface = file.takeIf { it.exists() }
+ ?.runCatching { Typeface.createFromFile(this) }
+ ?.getOrNull() ?: return
+
+ setLyricTypeface(typeface)
+ }
+
+ override fun setLyricTypeface(path: String) {
+ setLyricTypeface(File(path))
+ }
+
+ override fun setLyricTypeface(typeface: Typeface?) {
+ lyricPaint.typeface = typeface
+ secondLyricPaint.typeface = typeface
+ postInvalidate()
+ }
+
+ override fun setDampingRatioForLyric(dampingRatio: Float) {
+ dampingRatioForLyric = dampingRatio
+ progressSpringAnimator.spring.dampingRatio = dampingRatio
+ }
+
+ override fun setDampingRatioForViewPort(dampingRatio: Float) {
+ dampingRatioForViewPort = dampingRatio
+ viewPortSpringAnimator.spring.dampingRatio = dampingRatio
+ }
+
+ override fun setStiffnessForLyric(stiffness: Float) {
+ stiffnessForLyric = stiffness
+ progressSpringAnimator.spring.stiffness = stiffness
+ }
+
+ override fun setStiffnessForViewPort(stiffness: Float) {
+ stiffnessForViewPort = stiffness
+ viewPortSpringAnimator.spring.stiffness = stiffness
+ }
+
+ override fun setPlayDrawable(drawable: Drawable) {
+ playDrawable = drawable
+ }
+
+ override fun setIsDrawTranslation(isDrawTranslation: Boolean) {
+ this.isDrawTranslation = isDrawTranslation
+ postInvalidate()
+ }
+
+ override fun setIsEnableBlurEffect(isEnableBlurEffect: Boolean) {
+ this.isEnableBlurEffect.value = isEnableBlurEffect
+ postInvalidate()
+ }
+
+ override fun setItemOffsetPercent(itemOffsetPercent: Float) {
+ this.itemOffsetPercent = itemOffsetPercent
+ postInvalidate()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dirror/lyricviewx/ReadyHelper.kt b/app/src/main/java/com/dirror/lyricviewx/ReadyHelper.kt
new file mode 100644
index 0000000..9b81331
--- /dev/null
+++ b/app/src/main/java/com/dirror/lyricviewx/ReadyHelper.kt
@@ -0,0 +1,51 @@
+package com.dirror.lyricviewx
+
+import androidx.annotation.IntDef
+
+const val STATE_CREATED = 1
+const val STATE_INITIALIZING = 2
+const val STATE_INITIALIZED = 3
+const val STATE_ERROR = 4
+
+@IntDef(
+ STATE_CREATED,
+ STATE_INITIALIZING,
+ STATE_INITIALIZED,
+ STATE_ERROR
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class ReadyState
+
+/**
+ * 简单的状态机,根据 [readyState] 的状态决定当前任务的执行或延后与否
+ */
+open class ReadyHelper {
+ private var readyCallback: (Boolean) -> Unit = {}
+
+ @ReadyState
+ var readyState: Int = STATE_CREATED
+ set(value) {
+ if (field == value) return
+ when (value) {
+ STATE_INITIALIZED,
+ STATE_ERROR -> synchronized(readyCallback) {
+ field = value
+ readyCallback.invoke(value != STATE_ERROR)
+ }
+ else -> field = value
+ }
+ }
+
+ fun whenReady(performAction: (Boolean) -> Unit): Boolean {
+ return when (readyState) {
+ STATE_CREATED, STATE_INITIALIZING -> {
+ readyCallback = performAction
+ false
+ }
+ else -> {
+ performAction(readyState != STATE_ERROR)
+ true
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dirror/lyricviewx/extension/BlurMaskFilterExt.kt b/app/src/main/java/com/dirror/lyricviewx/extension/BlurMaskFilterExt.kt
new file mode 100644
index 0000000..f5a8558
--- /dev/null
+++ b/app/src/main/java/com/dirror/lyricviewx/extension/BlurMaskFilterExt.kt
@@ -0,0 +1,15 @@
+package com.dirror.lyricviewx.extension
+
+import android.graphics.BlurMaskFilter
+import android.util.SparseArray
+
+class BlurMaskFilterExt {
+ private val maskFilterCache = SparseArray()
+
+ fun get(radius: Int): BlurMaskFilter? {
+ if (radius == 0 || radius > 25) return null
+
+ return maskFilterCache[radius] ?: BlurMaskFilter(radius.toFloat(), BlurMaskFilter.Blur.NORMAL)
+ .also { maskFilterCache.put(radius, it) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/muqingbfq/MP3.java b/app/src/main/java/com/muqingbfq/MP3.java
new file mode 100644
index 0000000..00e9651
--- /dev/null
+++ b/app/src/main/java/com/muqingbfq/MP3.java
@@ -0,0 +1,50 @@
+package com.muqingbfq;
+
+import androidx.annotation.NonNull;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+public class MP3 implements Serializable {
+ public String id, name, zz, url;
+ // 音乐的贴图
+ public String picurl;
+
+
+ public MP3(String id, String name, String zz, String picurl) {
+ this.id = id;
+ this.name = name;
+ this.zz = zz;
+ this.picurl = picurl;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof MP3)) return false;
+ MP3 mp3 = (MP3) o;
+ if (id.equals(mp3.id)) {
+ return true;
+ }
+ return Objects.equals(id, mp3.id) &&
+ Objects.equals(name, mp3.name) &&
+ Objects.equals(zz, mp3.zz) &&
+ Objects.equals(picurl, mp3.picurl);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, zz, picurl);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "MP3{" +
+ "id='" + id + '\'' +
+ ", name='" + name + '\'' +
+ ", zz='" + zz + '\'' +
+ ", picurl=" + picurl +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/muqingbfq/MediaPlayer.java b/app/src/main/java/com/muqingbfq/MediaPlayer.java
new file mode 100644
index 0000000..00382cb
--- /dev/null
+++ b/app/src/main/java/com/muqingbfq/MediaPlayer.java
@@ -0,0 +1,170 @@
+package com.muqingbfq;
+
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioAttributes;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.target.Target;
+import com.mpatric.mp3agic.ID3v2;
+import com.mpatric.mp3agic.Mp3File;
+import com.muqingbfq.fragment.bflb_db;
+import com.muqingbfq.fragment.mp3;
+import com.muqingbfq.mq.gj;
+import com.muqingbfq.mq.wj;
+
+import java.io.IOException;
+
+public class MediaPlayer extends android.media.MediaPlayer {
+ // 每秒更新一次进度
+ @SuppressLint("UnsafeOptInUsageError")
+ public MediaPlayer() {
+ setOnErrorListener((mediaPlayer, i, i1) -> {
+ if (bfqkz.list.isEmpty()) {
+ return false;
+ }
+ //针对错误进行相应的处理
+ bfqkz.list.remove(bfqkz.xm);
+ bfqkz.xm = bfqkz.list.get(bfqkz.getmti());
+ new bfqkz.mp3(com.muqingbfq.api.
+ url.hq(bfqkz.xm));
+ return false;
+ });
+ setOnCompletionListener(mediaPlayer -> {
+ if (bfqkz.list.isEmpty()) {
+ return;
+ }
+ bfq_an.xyq();
+ });
+ setAudioAttributes(new AudioAttributes
+ .Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .build());
+// main.handler.post(updateSeekBar); // 在播放开始时启动更新进度
+ }
+
+ @Override
+ public void pause() throws IllegalStateException {
+ if (isPlaying()) {
+ super.pause();
+// bfq.isPlaying = false;
+ }
+
+ if (bfqkz.notify != null) {
+ bfqkz.notify.tzl_button();
+ }
+ }
+
+ @Override
+ public void start() throws IllegalStateException {
+ if (bfqkz.xm == null) {
+ if (bfqkz.list != null && !bfqkz.list.isEmpty()) {
+ bfq_an.xyq();
+ }
+ return;
+ }
+ super.start();
+
+ if (bfqkz.notify != null) {
+ bfqkz.notify.tzl_button();
+ }
+// bfq.isPlaying = true;
+ }
+
+
+ public void setDataSource(MP3 mp3) throws IOException {
+ reset();
+ super.setDataSource(mp3.url);
+ prepare();
+ start();
+ bfqkz.xm = mp3;
+ main.handler.post(() -> {
+ bfui();
+ if (bfqkz.notify != null) {
+ bfqkz.notify.tzl();
+ }
+ });
+
+ new Thread() {
+ @Override
+ public void run() {
+ super.run();
+ if (bfqkz.lishi_list.size() >= 100) {
+ bfqkz.lishi_list.remove(0);
+ }
+ bfqkz.lishi_list.remove(bfqkz.xm);
+ if (!bfqkz.lishi_list.contains(bfqkz.xm)) {
+ bfqkz.lishi_list.add(0, bfqkz.xm);
+ wj.xrwb(wj.gd + "mp3_hc.json", new com.google.gson.Gson().toJson(bfqkz.lishi_list));
+ }
+ wj.setMP3ToFile(bfqkz.xm);
+ }
+ }.start();
+ }
+
+ public void DataSource(MP3 path) throws Exception {
+ reset();
+ super.setDataSource(path.url);
+ prepare();
+ setTX();
+ }
+
+ public void setTX() {
+ Glide.with(main.application)
+ .asBitmap()
+ .load(bfqkz.xm.picurl)
+ .listener(new RequestListener() {
+ @Override
+ public boolean onLoadFailed(@Nullable GlideException e, Object model,
+ @NonNull Target target,
+ boolean isFirstResource) {
+ Bitmap bitmap = null;
+ try {
+ Mp3File mp3file = new Mp3File(bfqkz.xm.picurl);
+ if (mp3file.hasId3v2Tag()) {
+ ID3v2 id3v2Tag = mp3file.getId3v2Tag();
+ byte[] albumImage = id3v2Tag.getAlbumImage();
+ bitmap =
+ BitmapFactory.decodeByteArray(albumImage, 0, albumImage.length);
+
+ }
+ } catch (Exception a) {
+ gj.sc(getClass() + " yc:" + a);
+ }
+ if (bfqkz.notify != null) {
+ bfqkz.notify.setbitmap(bitmap);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onResourceReady(@NonNull Bitmap bitmap, @NonNull Object model, Target target,
+ @NonNull DataSource dataSource,
+ boolean isFirstResource) {
+ if (bfqkz.notify != null) {
+ bfqkz.notify.setbitmap(bitmap);
+ }
+ return false;
+ }
+ })
+ .submit();
+ }
+ @SuppressLint("NotifyDataSetChanged")
+ public void bfui() {
+ setTX();
+ if (bflb_db.adapter != null) {
+// bflb_db.adapter.
+ bflb_db.adapter.notifyDataSetChanged();
+ }
+ if (mp3.adapter != null) {
+ mp3.adapter.notifyDataSetChanged();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/muqingbfq/MyButtonClickReceiver.java b/app/src/main/java/com/muqingbfq/MyButtonClickReceiver.java
new file mode 100644
index 0000000..3b2feba
--- /dev/null
+++ b/app/src/main/java/com/muqingbfq/MyButtonClickReceiver.java
@@ -0,0 +1,213 @@
+package com.muqingbfq;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.Settings;
+import android.view.KeyEvent;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.muqingbfq.mq.FloatingLyricsService;
+import com.muqingbfq.mq.wj;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class MyButtonClickReceiver extends BroadcastReceiver {
+ private final Timer timer = new Timer();
+ private static int clickCount;
+
+ public MyButtonClickReceiver() {
+ super();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) {
+ int bluetoothState = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, 0);
+ //蓝牙断开
+ if (bluetoothState == BluetoothAdapter.STATE_DISCONNECTED) {
+ receiverPause();
+ }
+ return;
+ }
+ if (action.equals("android.intent.action.HEADSET_PLUG")) {
+ if (intent.hasExtra("state")) {
+ if (intent.getIntExtra("state", 2) == 0) {
+ //拔出
+ if (bfqkz.mt.isPlaying()) {
+ receiverPause();
+ }
+ } else if (intent.getIntExtra("state", 2) == 1) {
+ receiverPlay();
+ //插入
+ }
+ }
+ return;
+ }
+ if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
+ KeyEvent keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+ if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK && keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ clickCount = clickCount + 1;
+ if (clickCount == 1) {
+ HeadsetTimerTask headsetTimerTask = new HeadsetTimerTask();
+ timer.schedule(headsetTimerTask, 500);
+ }
+ } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_NEXT && keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ handler(2);
+ } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PREVIOUS && keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ handler(3);
+ } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PAUSE && keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ handler(4);
+ } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY && keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ handler(5);
+ }
+ return;
+ }
+
+ switch (action) {
+ case "kg":
+ playOrPause();
+ break;
+ case "syq":
+ bfq_an.syq();
+ break;
+ case "xyq":
+ bfq_an.xyq();
+ break;
+ case "lrc":
+ if (FloatingLyricsService.lei == null) {
+ if (!Settings.canDrawOverlays(main.application)) {
+ // 无权限,需要申请权限
+ main.application.startActivity(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+ Uri.parse("package:" + main.application.getPackageName())));
+ } else {
+ main.application.startService(
+ new Intent(main.application, FloatingLyricsService.class));
+ }
+ return;
+ }
+ FloatingLyricsService lei = FloatingLyricsService.lei;
+ if (lei.setup.i == 1) {
+ lei.setyc();
+ } else {
+ lei.setup.i = 0;
+ lei.baocun();
+ main.application.stopService(
+ new Intent(main.application, FloatingLyricsService.class));
+ }
+ break;
+ case "like":
+ try {
+ Gson gson = new Gson();
+ Type type = new TypeToken>() {
+ }.getType();
+ List list = gson.fromJson(wj.dqwb(wj.gd + "mp3_like.json"), type);
+ if (list == null) {
+ list = new ArrayList<>();
+ }
+ if (bfqkz.like_bool) {
+ list.remove(bfqkz.xm);
+ bfqkz.like_bool = false;
+ } else {
+ if (!list.contains(bfqkz.xm)) {
+ list.add(bfqkz.xm);
+ bfqkz.like_bool = true;
+ }
+ }
+ wj.xrwb(wj.gd + "mp3_like.json", gson.toJson(list));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ break;
+ }
+ // 处理按钮点击事件的逻辑
+ }
+
+ class HeadsetTimerTask extends TimerTask {
+ @Override
+ public void run() {
+ try {
+ if (clickCount == 1) {
+ handler(1);
+ } else if (clickCount == 2) {
+ handler(2);
+ } else if (clickCount >= 3) {
+ handler(3);
+ }
+ clickCount = 0;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ private void handler(int a) {
+ switch (a) {
+ case 1:
+ playOrPause();
+ break;
+ case 2:
+ playNext();
+ break;
+ case 3:
+ playPrevious();
+ break;
+ case 4:
+ receiverPause();
+ break;
+ case 5:
+ receiverPlay();
+ break;
+ default:
+ break;
+ }
+ }
+
+
+ // * 对蓝牙 播放
+ public void receiverPlay() {
+ bfqkz.mt.start();
+ }
+
+ // * 对蓝牙 暂停
+ public void receiverPause() {
+ bfqkz.mt.pause();
+ }
+
+ /**
+ * 对蓝牙 播放-暂停
+ */
+ public static void playOrPause() {
+ if (bfqkz.mt == null) {
+ return;
+ }
+// gj.sc(isMusicServiceBound);播放/暂停按钮点击事件 if (isMusicServiceBound)
+ if (bfqkz.mt.isPlaying()) {
+ bfqkz.mt.pause();
+ } else {
+ bfqkz.mt.start();
+ }
+ bfqkz.notify.tzl();
+ }
+
+ /**
+ * 对蓝牙 下一首
+ */
+ public void playNext() {
+ bfq_an.xyq();
+ }
+
+ /**
+ * 对蓝牙 上一首
+ */
+ public void playPrevious() {
+ bfq_an.syq();
+ }
+}
diff --git a/app/src/main/java/com/muqingbfq/XM.java b/app/src/main/java/com/muqingbfq/XM.java
new file mode 100644
index 0000000..5f44994
--- /dev/null
+++ b/app/src/main/java/com/muqingbfq/XM.java
@@ -0,0 +1,22 @@
+package com.muqingbfq;
+
+public class XM {
+ public String id, name, message;
+ public Object picurl;
+ public XM(String id, String name, String picurl) {
+ this.id = id;
+ this.name = name;
+ this.picurl = picurl;
+ }
+ public XM(String id, String name,String message, String picurl) {
+ this.id = id;
+ this.name = name;
+ this.picurl = picurl;
+ this.message = message;
+ }
+ public XM(String id, String name, int picurl) {
+ this.id = id;
+ this.name = name;
+ this.picurl = picurl;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/muqingbfq/activity_about_software.java b/app/src/main/java/com/muqingbfq/activity_about_software.java
new file mode 100644
index 0000000..3324292
--- /dev/null
+++ b/app/src/main/java/com/muqingbfq/activity_about_software.java
@@ -0,0 +1,185 @@
+package com.muqingbfq;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.Html;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.Toolbar;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.google.android.material.bottomsheet.BottomSheetDialog;
+import com.google.android.material.bottomsheet.BottomSheetDragHandleView;
+import com.muqingbfq.databinding.ActivityAboutSoftwareBinding;
+import com.muqingbfq.databinding.ListKaifazheBinding;
+import com.muqingbfq.mq.AppCompatActivity;
+import com.muqingbfq.mq.gj;
+import com.muqingbfq.mq.wj;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+public class activity_about_software extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView();
+ try {
+ String versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
+ binding.text2.setText(String.format("%s Bate", versionName));
+ } catch (PackageManager.NameNotFoundException e) {
+ yc.start(this, e);
+ }
+ setSupportActionBar(findViewById(R.id.toolbar));
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ findViewById(R.id.button1).setOnClickListener(view -> {
+ new Thread() {
+ @Override
+ public void run() {
+ super.run();
+ int jianchagengxin = gj.jianchagengxin(activity_about_software.this);
+ if (jianchagengxin == 400) {
+ gj.xcts(activity_about_software.this, "无网络");
+ } else if (jianchagengxin == 0) {
+ gj.xcts(activity_about_software.this, "已经是最新的客户端了");
+ }
+ }
+ }.start();
+ });
+ TextView viewById = findViewById(R.id.text1);
+ AssetManager assets = getAssets();
+ try {
+ InputStream open = assets.open("about.html");
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(open));
+ StringBuilder stringBuilder = new StringBuilder();
+ String ling;
+ while ((ling = bufferedReader.readLine()) != null) {
+ stringBuilder.append(ling);
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ viewById.setText(Html.fromHtml(stringBuilder.toString(), Html.FROM_HTML_MODE_LEGACY));
+ } else {
+ viewById.setText(Html.fromHtml(stringBuilder.toString()));
+ }
+ open.close();
+ bufferedReader.close();
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ viewById.setText(String.format("错误:%s", e));
+ }
+ }
+
+ MenuItem itemA;
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ itemA= menu.add("特别鸣谢");
+ itemA.setTitle("特别鸣谢");
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == android.R.id.home) {
+ finish();
+ } else if (item == itemA) {
+ new botton(this);
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected ActivityAboutSoftwareBinding getViewBindingObject(LayoutInflater layoutInflater) {
+ return ActivityAboutSoftwareBinding.inflate(layoutInflater);
+ }
+
+ class botton extends BottomSheetDialog {
+
+ List