commit 756cd6e75c50ff4cca7a560c414121f6ba441d60
Author: muqing <1966944300@qq.com>
Date: Wed Jan 15 18:55:44 2025 +0800
从GitHub移植过来
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dfc8a39
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+*.apk
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c3c7e1a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+## 简介
+一个对接网易云的音乐播放器
+
+## 截图
+暂无
+## 联系作者
+ * QQ:1966944300
+ * 邮箱:1966944300@qq.com
+
+## 后台
+ * Github: [网易云音乐 API](https://github.com/Binaryify/NeteaseCloudMusicApi)
+
+## 关于
+在兴趣的驱动下,写一个`免费`的东西,有欣喜,也还有汗水,希望你喜欢我的作品,同时也能支持一下。
+
+## 修改JAR的包
+ (主要修改内容MD3化)
+ com.github.QuadFlask:colorpicker:0.0.15
+ # (歌词做全局变量给悬浮窗歌词)
+ 'com.github.wangchenyan:lrcview:2.2.1'
\ 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..4e0cdf4
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,94 @@
+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 34
+ defaultConfig {
+ applicationId "com.muqingbfq"
+ minSdk 24
+ //noinspection ExpiredTargetSdkVersion,OldTargetApi
+ targetSdk 34
+ 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.7.0'
+ implementation 'com.google.android.material:material:1.10.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 'jp.wasabeef:glide-transformations:4.3.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'
+
+// 沉浸式状态栏
+ // release 版本
+ // 将 $lastVersion 替换成上图中的版本号
+ implementation 'com.gitee.zackratos:UltimateBarX:v0.8.1'
+// api project(path: ':lrcview')
+ //歌词组件库
+ api "com.github.cy745:EaseView:e11c3208a9"
+ api "androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03"
+
+ implementation "androidx.media3:media3-exoplayer:1.4.0"
+ implementation "androidx.media3:media3-ui:1.4.0"
+ implementation "androidx.media3:media3-common:1.4.0"
+ implementation "androidx.media3:media3-session:1.4.0"
+ implementation 'androidx.activity:activity:1.9.1'
+
+ implementation "androidx.lifecycle:lifecycle-process:2.6.1"
+
+
+
+}
\ 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/release/output-metadata.json b/app/release/output-metadata.json
new file mode 100644
index 0000000..c9b125f
--- /dev/null
+++ b/app/release/output-metadata.json
@@ -0,0 +1,20 @@
+{
+ "version": 3,
+ "artifactType": {
+ "type": "APK",
+ "kind": "Directory"
+ },
+ "applicationId": "com.muqingbfq",
+ "variantName": "release",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "attributes": [],
+ "versionCode": 1,
+ "versionName": "2.6.0",
+ "outputFile": "Cloud_music-release-v2.6.0.apk"
+ }
+ ],
+ "elementType": "File"
+}
\ 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..376c193
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..3d9c024
--- /dev/null
+++ b/app/src/main/java/com/dirror/lyricviewx/LyricViewX.kt
@@ -0,0 +1,1093 @@
+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/HomeSteer.java b/app/src/main/java/com/muqingbfq/HomeSteer.java
new file mode 100644
index 0000000..b166b27
--- /dev/null
+++ b/app/src/main/java/com/muqingbfq/HomeSteer.java
@@ -0,0 +1,123 @@
+package com.muqingbfq;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.text.Editable;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.muqingbfq.fragment.wode;
+import com.muqingbfq.login.user_logs;
+import com.muqingbfq.mq.EditViewDialog;
+import com.muqingbfq.mq.gj;
+import com.muqingbfq.mq.wl;
+import com.muqingbfq.view.Edit;
+
+import org.json.JSONObject;
+
+public class HomeSteer {
+ home home;
+ ActivityResultLauncher dlintent;
+
+ public HomeSteer(home home) {
+ this.home = home;
+ dlintent = home.registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == Activity.RESULT_OK) {
+ // 处理返回结果
+ Intent data = result.getData();
+ boolean bool = data.getBooleanExtra("bool", false);
+ if (bool) {
+ Yes();
+ return;
+ }
+ }
+ One();
+ });
+ SetIP();
+// One();
+
+ }
+
+ public void One() {
+ MaterialAlertDialogBuilder materialAlertDialogBuilder = new MaterialAlertDialogBuilder(home);
+ materialAlertDialogBuilder.setTitle("引导登陆");
+ materialAlertDialogBuilder.setItems(new String[]{"游客", "登陆"}, (dialog, which) -> {
+ if (which == 0) {
+ new Thread() {
+ @Override
+ public void run() {
+ super.run();
+ //获取游客Cookie
+ String hq = wl.hq("/register/anonimous");
+ try {
+ JSONObject jsonObject = new JSONObject(hq);
+ wl.setcookie(jsonObject.getString("cookie"));
+ home.runOnUiThread(() -> Yes());
+ } catch (Exception e) {
+ home.runOnUiThread(() -> Toast.makeText(home, "游客登陆失败:" + e.getMessage(), Toast.LENGTH_SHORT).show());
+ gj.sc(e);
+ }
+ }
+ }.start();
+ } else if (which == 1) {
+ dlintent.launch(new Intent(home, user_logs.class));
+ }
+ });
+ materialAlertDialogBuilder.show();
+ }
+
+ public void Yes() {
+
+ }
+
+ /**
+ * 设置IP地址
+ */
+ public void SetIP() {
+ SharedPreferences nickname = home.getSharedPreferences("Set_up", Context.MODE_PRIVATE);
+ if (nickname.getString("IP", "").isEmpty()) {
+ EditViewDialog editViewDialog = new EditViewDialog(home, "IP");
+ editViewDialog.setMessage("请输入部署了NeteaseCloudMusicApi的服务器地址,\n例如" +
+ "https://api.csm.sayqz.com");
+// editViewDialog.setPositive()
+ editViewDialog.buttonb.setEnabled(false);
+ editViewDialog.editText.setMaxLines(1);
+ editViewDialog.editText.addTextChangedListener(new Edit.TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence var1, int var2, int var3, int var4) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence var1, int var2, int var3, int var4) {
+ //正则表达式检查是否为 https://api.csm.sayqz.com这样的
+ editViewDialog.buttonb.setEnabled(var1.toString().matches("^(https?://).+[^/]"));
+ }
+
+ @Override
+ public void afterTextChanged(Editable var1) {
+
+ }
+ });
+ editViewDialog.setPositive(v -> {
+ main.api = editViewDialog.getEditText();
+ nickname.edit().putString("IP", editViewDialog.getEditText()).apply();
+ One();
+ editViewDialog.dismiss();
+ });
+ editViewDialog.show();
+ } else {
+ main.api = nickname.getString("IP", "");
+ One();
+ }
+ }
+}
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..ae8b148
--- /dev/null
+++ b/app/src/main/java/com/muqingbfq/MP3.java
@@ -0,0 +1,56 @@
+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(){}
+
+
+ public MP3(String id) {
+ this.id = id;
+ }
+
+ 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 +
+ ",url=" + url +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/muqingbfq/PlaybackService.java b/app/src/main/java/com/muqingbfq/PlaybackService.java
new file mode 100644
index 0000000..a2f31aa
--- /dev/null
+++ b/app/src/main/java/com/muqingbfq/PlaybackService.java
@@ -0,0 +1,217 @@
+package com.muqingbfq;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media3.common.C;
+import androidx.media3.common.MediaItem;
+import androidx.media3.common.MediaMetadata;
+import androidx.media3.common.PlaybackException;
+import androidx.media3.common.Player;
+import androidx.media3.common.Tracks;
+import androidx.media3.common.util.UnstableApi;
+import androidx.media3.exoplayer.ExoPlayer;
+import androidx.media3.session.MediaSession;
+import androidx.media3.session.MediaSessionService;
+
+import com.google.common.base.Strings;
+import com.google.gson.Gson;
+import com.muqingbfq.api.url;
+import com.muqingbfq.mq.gj;
+import com.muqingbfq.mq.wj;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class PlaybackService extends MediaSessionService {
+ public static MediaSession mediaSession = null;
+ public static List list = new ArrayList<>();
+
+ //历史记录
+// public static final List listHistory = new ArrayList<>();
+
+ public static void ListSave() {
+ new Thread(() -> wj.xrwb(wj.filesdri + "list.json", new Gson().toJson(list))).start();
+ }
+
+
+ public Player.Listener PlayerListener = new Player.Listener() {
+ @Override
+ public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
+ // 处理 playWhenReady 的变化
+ }
+
+ @Override
+ public void onPlaybackStateChanged(int playbackState) {
+ // 处理播放状态的变化
+ if (playbackState == Player.STATE_ENDED) {
+ // 检查当前播放的媒体项是否是最后一项
+ Player player = mediaSession.getPlayer();
+ if (player.getCurrentMediaItemIndex() == player.getMediaItemCount() - 1) {
+ // 如果是最后一项,回到第一项并播放
+ player.seekTo(0,0);
+ player.play();
+ }
+ }
+
+ }
+
+ @Override
+ public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) {
+ // 处理位置不连续变化
+ }
+
+ @Override
+ public void onMediaItemTransition(@Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) {
+ if (mediaItem != null) {
+ // 输出当前的 MediaItem 信息
+ String title = mediaItem.mediaMetadata.title != null ? mediaItem.mediaMetadata.title.toString() : "未知标题";
+ String artist = mediaItem.mediaMetadata.artist != null ? mediaItem.mediaMetadata.artist.toString() : "未知艺术家";
+
+// bfqkz.lishi_list.removeIf(mp3 -> mp3.id.equals(mediaItem.mediaId));
+// bfqkz.lishi_list.add(0, new MP3(mediaItem.mediaId, title, artist, mediaItem.mediaMetadata.artworkUri.toString()));
+// new Thread(() -> wj.xrwb(wj.gd + "mp3_hc.json", new Gson().toJson(bfqkz.lishi_list))).start();
+ }
+ }
+ @Override
+ public void onTracksChanged(@Nullable Tracks tracks) {
+ gj.sc(tracks);
+ // Update UI using current tracks.
+ }
+
+ int error_count = 0;//设置错误次数
+ MediaItem currentMediaItem = null;
+ int currentIndex = 0;
+
+ @Override
+ public void onPlayerError(@NonNull PlaybackException error) {
+ // 当播放发生错误时调用
+ // 如果错误是由于资源找不到
+ Player player = mediaSession.getPlayer();
+ if (++error_count > 3) {
+ currentIndex = player.getNextMediaItemIndex();
+ if (currentIndex != C.INDEX_UNSET) {
+ gj.sc("播放失败,已跳过");
+ return;
+ } else {
+ currentMediaItem = player.getMediaItemAt(currentIndex); // 获取下一首的 MediaItem
+ }
+ } else {
+ currentMediaItem = player.getCurrentMediaItem();
+ currentIndex = player.getCurrentMediaItemIndex(); // 获取当前播放项的索引
+ }
+ new Thread(() -> {
+ MP3 hq = url.hq(new MP3(currentMediaItem.mediaId));
+ hq.picurl = url.picurl(hq.id);
+ // 设置新的 MediaItem
+ MediaItem newMediaItem = currentMediaItem.buildUpon()
+ .setUri(hq.url) // 更新 URI
+ .build(); // 构建新的 MediaItem
+ main.handler.post(() -> {
+ player.replaceMediaItem(currentIndex, newMediaItem);
+ player.prepare();
+ player.play();
+ error_count = 0;
+ });
+ }).start();
+ }
+ };
+
+
+ @UnstableApi
+ public void onCreate() {
+ super.onCreate();
+ ExoPlayer player = new ExoPlayer.Builder(this)
+ .build();
+ mediaSession = new MediaSession.Builder(this, player).build();
+
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setComponent(new ComponentName(this, home.class));//用ComponentName得到class对象
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);// 关键的一步,设置启动模式,两种情况
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ mediaSession.setSessionActivity(pendingIntent);
+// 添加 ExoPlayer 监听器
+ player.addListener(PlayerListener);
+
+
+ String nickname = wj.dqwb(wj.filesdri + "list.json");
+ if (!Strings.isNullOrEmpty(nickname)) {
+ try {
+ list = new Gson().fromJson(nickname, new com.google.gson.reflect.TypeToken>() {
+ }.getType());
+ for (MP3 mediaItem : list) {
+ player.addMediaItem(PlaybackService.GetMp3(mediaItem));
+ }
+ } catch (Exception e) {
+ gj.sc(e);
+ list = new ArrayList<>();
+ wj.sc(wj.filesdri + "list.json");
+ }
+ }
+
+ }
+
+ @Nullable
+ @Override
+ public MediaSession onGetSession(@NonNull MediaSession.ControllerInfo controllerInfo) {
+ return mediaSession;
+ }
+
+ @Override
+ public void onDestroy() {
+ mediaSession.getPlayer().release();
+ mediaSession.release();
+ mediaSession = null;
+ super.onDestroy();
+ }
+
+ public static MediaItem GetMp3(MP3 mp3) {
+ // 创建媒体的元数据(如标题、描述、图片等)
+ MediaMetadata mediaMetadata = new MediaMetadata.Builder()
+ .setTitle(mp3.name)
+ .setArtist(mp3.zz)
+ .setAlbumTitle(mp3.zz)
+ .setArtworkUri(Uri.parse(mp3.picurl)) // 图片URL
+ .build();
+// 创建带有元数据的 MediaItem
+
+ return new MediaItem.Builder()
+ .setMediaId(mp3.id) // 设置媒体的唯一ID
+ .setUri(Strings.isNullOrEmpty(mp3.url) ? "" : mp3.url)
+ .setMediaMetadata(mediaMetadata) // 将元数据添加到 MediaItem
+ .build();
+ }
+
+ public static void AddMediaItem(MP3 mp3) {
+ if (mediaSession != null) {
+ new Thread(() -> {
+ MediaItem mediaItem = GetMp3(mp3);
+ main.handler.post(() -> {
+ Player player = mediaSession.getPlayer();
+ for (int i = 0; i < player.getMediaItemCount(); i++) {
+ MediaItem existingItem = player.getMediaItemAt(i);
+ if (Objects.equals(existingItem.mediaId, mp3.id)) {
+ player.seekTo(i, 0);
+ return;
+ }
+ }
+ player.addMediaItem(0, mediaItem);
+ player.seekTo(0, 0);
+ player.prepare();
+ player.play();
+ });
+ }).start();
+ }
+
+ }
+
+
+}
\ No newline at end of file
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/Music.java b/app/src/main/java/com/muqingbfq/activity/Music.java
new file mode 100644
index 0000000..cacbd87
--- /dev/null
+++ b/app/src/main/java/com/muqingbfq/activity/Music.java
@@ -0,0 +1,544 @@
+package com.muqingbfq.activity;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.LayerDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.SeekBar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.media3.common.MediaItem;
+import androidx.media3.common.MediaMetadata;
+import androidx.media3.common.Player;
+import androidx.media3.common.util.Util;
+import androidx.palette.graphics.Palette;
+
+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.RequestOptions;
+import com.bumptech.glide.request.target.Target;
+import com.muqingbfq.MP3;
+import com.muqingbfq.PlaybackService;
+import com.muqingbfq.R;
+import com.muqingbfq.bfq_an;
+import com.muqingbfq.bfqkz;
+import com.muqingbfq.databinding.ActivityMusicBinding;
+import com.muqingbfq.fragment.Media;
+import com.muqingbfq.main;
+import com.muqingbfq.mq.AppCompatActivity;
+import com.muqingbfq.mq.MusicViewModel;
+import com.muqingbfq.mq.gj;
+
+
+public class Music extends AppCompatActivity implements GestureDetector.OnGestureListener {
+
+ private Player player = PlaybackService.mediaSession.getPlayer();
+ private int TdtHeight = 15;
+ public static Bitmap backgroundbitmap=null;
+
+ public static void startActivity(Context context, MP3 mp3) {
+ Intent intent = new Intent(context, Music.class);
+ intent.putExtra("MP3", mp3);
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ context.startActivity(intent);
+ }
+
+ public static void startActivity(Context context) {
+ Intent intent = new Intent(context, Music.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ context.startActivity(intent);
+ }
+
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+
+ GestureDetector gestureDetector;
+ float Minfloat = 1000f;
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ gestureDetector = new GestureDetector(this, this);
+ setContentView();
+ color(backgroundbitmap);
+
+ // 获取屏幕的高度
+ WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
+ windowManager.getDefaultDisplay().getMetrics(displayMetrics);
+ ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+// 创建新的MarginLayoutParams
+ ViewGroup.LayoutParams layoutParams = binding.toolbar.getLayoutParams();
+// 设置margin
+ ((ViewGroup.MarginLayoutParams) layoutParams).setMargins(0, systemBars.top, 0, 0);
+ binding.toolbar.setLayoutParams(layoutParams);
+ if (gj.isTablet(this)) {
+// binding.image2.setLayoutParams(layoutParams);
+ ViewGroup.LayoutParams layoutParams1 = binding.cardview.getLayoutParams();
+ layoutParams1.width = displayMetrics.heightPixels / 2;
+ layoutParams1.height = displayMetrics.heightPixels / 2;
+
+ }
+ return insets;
+ });
+
+ Minfloat = displayMetrics.heightPixels - displayMetrics.heightPixels / 3f;
+ binding.kg.setOnClickListener(view -> Util.handlePlayPauseButtonAction(player));
+ binding.xyq.setOnClickListener(v -> {
+ boolean b = player.hasNextMediaItem();
+ if (b) {
+ player.seekToNextMediaItem();
+ } else {
+ gj.ts(v.getContext(), "已经是最后一首了");
+ }
+ });
+ binding.syq.setOnClickListener(v -> {
+ boolean b = player.hasPreviousMediaItem();
+ if (b) {
+ player.seekToPreviousMediaItem();
+ } else {
+ gj.ts(v.getContext(), "已经是第一首了");
+ }
+ });
+ player.addListener(Listener);
+ if (PlaybackService.mediaSession != null) {
+ updateUI(player);
+ }
+ binding.tdt.post(() -> TdtHeight = binding.tdt.getHeight());
+ binding.tdt.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ long actualPosition = (progress * player.getDuration()) / 100;
+ String time = bfq_an.getTime(actualPosition);
+ binding.timeB.setText(time);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // 获取 View 当前的高度;
+ // 创建一个 ValueAnimator,从当前高度逐渐增加到目标高度
+ ValueAnimator animator = ValueAnimator.ofInt(TdtHeight, TdtHeight + 10);
+ animator.setDuration(300); // 设置动画持续时间为 300 毫秒
+ animator.setInterpolator(new DecelerateInterpolator()); // 设置动画插值器
+ // 在动画过程中更新 View 的高度
+ animator.addUpdateListener(animation -> {
+ // 获取当前动画的值(高度)
+ int animatedValue = (int) animation.getAnimatedValue();
+
+ // 更新 View 的高度
+ ViewGroup.LayoutParams layoutParams = seekBar.getLayoutParams();
+ layoutParams.height = animatedValue;
+ seekBar.setLayoutParams(layoutParams);
+ });
+
+ // 开始动画
+ animator.start();
+ isDrag = true;
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ isDrag = false;
+ int progress = seekBar.getProgress();
+ if (progress >= 100) {
+ player.seekToNextMediaItem();
+ } else {
+
+ long actualPosition = (progress * player.getDuration()) / 100;
+ player.seekTo(actualPosition);
+ }
+
+
+ ValueAnimator animator = ValueAnimator.ofInt(TdtHeight, TdtHeight - 10);
+ animator.setDuration(300); // 设置动画持续时间为 300 毫秒
+ animator.setInterpolator(new DecelerateInterpolator()); // 设置动画插值器
+ // 在动画过程中更新 View 的高度
+ animator.addUpdateListener(animation -> {
+ // 获取当前动画的值(高度)
+ int animatedValue = (int) animation.getAnimatedValue();
+ // 更新 View 的高度
+ ViewGroup.LayoutParams layoutParams = seekBar.getLayoutParams();
+ layoutParams.height = animatedValue;
+ seekBar.setLayoutParams(layoutParams);
+ });
+
+ // 开始动画
+ animator.start();
+ }
+ });
+ binding.back.setOnClickListener(v -> finish());
+
+ binding.fragmentBfq.setOnClickListener(v -> {
+ if (binding.cardview.getVisibility() == View.VISIBLE) {
+ binding.cardview.setVisibility(View.GONE);
+ binding.lrcView.setVisibility(View.VISIBLE);
+ } else {
+ binding.cardview.setVisibility(View.VISIBLE);
+ binding.lrcView.setVisibility(View.GONE);
+ }
+ });
+ binding.lrcView.setNormalTextSize(80f);
+ binding.lrcView.setCurrentTextSize(100f);
+ binding.lrcView.setTranslateTextScaleValue(0.8f);
+ binding.lrcView.setHorizontalOffset(-50f);
+ binding.lrcView.setHorizontalOffsetPercent(0.5f);
+ binding.lrcView.setItemOffsetPercent(0.5f);
+ binding.lrcView.setIsDrawTranslation(true);
+ binding.lrcView.setIsEnableBlurEffect(true);
+// binding.cardview.setLayoutParams(layoutParams);
+
+ binding.lrcView.setDraggable(true, time -> {
+ player.seekTo(time);
+ return false;
+ });
+
+ binding.lrcView.setOnSingerClickListener(() -> switchViews(binding.lrcView, binding.cardview));
+
+ binding.fragmentBfq.setOnTouchListener((v, event) -> {
+ gestureDetector.onTouchEvent(event);
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ if (binding.getRoot().getRootView().getTranslationY() > (getResources().getDisplayMetrics().heightPixels / 2.0f)) {
+ finish();
+ return true;
+ }
+ ObjectAnimator animator = ObjectAnimator.ofFloat(binding.getRoot().getRootView()
+ , "y", binding.getRoot().getRootView().getTranslationY(), 0);
+ animator.setDuration(500);
+ animator.start();
+ }
+ return true;
+ });
+
+ //播放列表
+ binding.bfqListMp3.setOnClickListener(v -> com.muqingbfq.fragment.bflb_db.start(v.getContext()));
+
+ }
+
+ //是否拖动
+ private boolean isDrag = false;
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ if (player != null && !isDrag) {
+ // 获取当前进度和持续时间
+ long currentPosition = player.getCurrentPosition();
+ long duration = player.getDuration();
+
+ // 更新进度条(假设有一个 SeekBar 名为 seekBar)
+ if (duration > 0) {
+ int progress = (int) ((currentPosition * 100) / duration);
+// gj.sc(progress);
+ if (progress < 1) {
+ progress = 1;
+ }
+ binding.tdt.setProgress(progress);
+// binding.tdt.setMax(100);
+ binding.lrcView.updateTime(currentPosition, true);
+ binding.timeA.setText(bfq_an.getTime(duration));
+ binding.timeB.setText(bfq_an.getTime(currentPosition));
+ }
+ }
+ // 计划下一次更新
+ main.handler.postDelayed(this, 1000); // 每秒更新一次
+ }
+ };
+
+ @Override
+ public void finish() {
+ super.finish();
+ player.removeListener(Listener);
+ }
+
+ private Player.Listener Listener = new Player.Listener() {
+ @Override
+ public void onEvents(@NonNull Player player, @NonNull Player.Events events) {
+ // 监听播放状态变化
+ if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
+ boolean isPlaying = player.getPlayWhenReady();
+ if (isPlaying) {
+ gj.sc("正在播放");
+ } else {
+ gj.sc("播放暂停");
+ }
+ }
+
+ // 监听下一曲事件
+ if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
+ MediaItem currentMediaItem = player.getCurrentMediaItem();
+ if (currentMediaItem != null) {
+ gj.sc("播放下一曲: ");
+ }
+ }
+
+ // 监听上一曲事件(通常通过手动调用控制播放器的skipToPrevious方法实现)
+ if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
+ // 你的逻辑代码,处理上一曲
+ gj.sc("播放上一曲");
+ }
+
+ if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
+ int playbackState = player.getPlaybackState();
+
+ switch (playbackState) {
+ case Player.STATE_READY:
+ if (player.getPlayWhenReady()) {
+ if (bfqkz.lrc != null) {
+ String[] strings = Media.loadLyric();
+ binding.lrcView.loadLyric(strings[0], strings[1]);
+ }
+
+ gj.sc("播放开始");
+ }
+ break;
+ case Player.STATE_ENDED:
+ gj.sc("播放结束");
+ break;
+ default:
+ // 处理其他状态
+ break;
+ }
+ }
+ updateUI(player);
+
+ }
+ };
+
+ @Override
+ protected ActivityMusicBinding getViewBindingObject(LayoutInflater layoutInflater) {
+ return ActivityMusicBinding.inflate(layoutInflater);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ main.handler.post(runnable);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // 取消所有未完成的任务
+ main.handler.removeCallbacks(runnable);
+ }
+
+
+ private void updateUI(Player player) {
+ boolean shouldShowPlayButton = Util.shouldShowPlayButton(player);
+ binding.kg.setImageResource(shouldShowPlayButton ? R.drawable.zt : R.drawable.bf);
+ // 获取当前播放的 MediaItem
+ MediaItem currentMediaItem = player.getCurrentMediaItem();
+ if (currentMediaItem != null && !isFinishing() && !isDestroyed()) {
+ MediaMetadata metadata = currentMediaItem.mediaMetadata;
+ String title = metadata.title != null ? metadata.title.toString() : "没有名字的音乐?";
+ String artist = metadata.artist != null ? metadata.artist.toString() : "未知艺术家";
+ binding.name.setText(title);
+ binding.zz.setText(artist);
+ SetBackGround(metadata.artworkUri);
+ }
+
+ }
+
+
+ private void SetBackGround(Uri artworkUri){
+ Glide.with(this)
+ .asBitmap()
+ .load(artworkUri)
+ .apply(new RequestOptions()
+ .placeholder(R.drawable.ic_launcher_foreground)
+ .error(R.drawable.ic_launcher_foreground))
+ .addListener(new RequestListener() {
+ @Override
+ public boolean onLoadFailed(@Nullable GlideException e, @Nullable Object model, @NonNull Target target, boolean isFirstResource) {
+ return true;
+ }
+
+ @Override
+ public boolean onResourceReady(@NonNull Bitmap resource,
+ @NonNull Object model, Target target,
+ @NonNull DataSource dataSource,
+ boolean isFirstResource) {
+ color(resource);
+ backgroundbitmap = resource;
+ binding.cardview.imageView.setImageBitmap(resource);
+ return true;
+ }
+ }).into(binding.cardview.imageView);
+ }
+
+
+ private void color(Bitmap bitmap) {
+ if (bitmap == null) {
+ return;
+ }
+ Palette.Builder builder = new Palette.Builder(bitmap);
+ builder.generate(palette -> {
+// 获取图片中柔和的亮色
+ int lightMutedColor = palette.getLightMutedColor(Color.GRAY);
+ Palette.Swatch vibrantSwatch = palette.getLightVibrantSwatch();
+ if (vibrantSwatch != null) {
+ int bodyTextColor = vibrantSwatch.getBodyTextColor();
+ binding.lrcView.setCurrentColor(bodyTextColor);
+ binding.lrcView.setTimelineTextColor(bodyTextColor);
+// 计算半亮度的颜色(直接将RGB分量除以2并向下取整)
+ int halfBrightnessColor = (bodyTextColor & 0x00FFFFFF) / 2;
+ binding.lrcView.setNormalColor(halfBrightnessColor);
+
+ GradientDrawable gradientDrawable = new GradientDrawable(
+ GradientDrawable.Orientation.BOTTOM_TOP, // 渐变方向:从上到下
+ new int[]{vibrantSwatch.getRgb(), lightMutedColor} // 渐变颜色数组
+ );
+ setTint(vibrantSwatch.getTitleTextColor());
+ gradientDrawable.setShape(GradientDrawable.RECTANGLE);
+ binding.getRoot().setBackground(gradientDrawable);
+ } else {
+ int color = palette.getLightVibrantColor(Color.WHITE);
+ int titleTextColor = palette.getVibrantColor(Color.GRAY);
+ binding.lrcView.setCurrentColor(titleTextColor);
+ binding.lrcView.setTimelineTextColor(titleTextColor);
+// 计算半亮度的颜色(直接将RGB分量除以2并向下取整)
+ int halfBrightnessColor = (titleTextColor & 0x00FFFFFF) / 2;
+ binding.lrcView.setNormalColor(halfBrightnessColor);
+
+ GradientDrawable gradientDrawable = new GradientDrawable(
+ GradientDrawable.Orientation.BOTTOM_TOP, // 渐变方向:从上到下
+ new int[]{color, lightMutedColor} // 渐变颜色数组
+ );
+ setTint(titleTextColor);
+ gradientDrawable.setShape(GradientDrawable.RECTANGLE);
+ binding.getRoot().setBackground(gradientDrawable);
+ }
+ });
+ }
+
+ private void setTint(int color) {
+// this.ColorTint = color;
+ ColorStateList colorStateList = ColorStateList.valueOf(color);
+ binding.kg.setImageTintList(colorStateList);
+ binding.syq.setImageTintList(colorStateList);
+ binding.xyq.setImageTintList(colorStateList);
+ binding.bfqListMp3.setImageTintList(colorStateList);
+ binding.control.setImageTintList(colorStateList);
+ binding.like.setImageTintList(colorStateList);
+ binding.download.setImageTintList(colorStateList);
+ binding.image2.setImageTintList(colorStateList);
+ binding.name.setTextColor(color);
+ binding.zz.setTextColor(color);
+ binding.timeA.setTextColor(color);
+ binding.timeB.setTextColor(color);
+
+ Drawable progressDrawable = binding.tdt.getProgressDrawable();
+ LayerDrawable layerDrawable = (LayerDrawable) progressDrawable;
+ Drawable progress = layerDrawable.findDrawableByLayerId(android.R.id.progress);
+ progress.setColorFilter(color, PorterDuff.Mode.SRC_IN);
+// 设置进度条背景的颜色
+ Drawable background = layerDrawable.findDrawableByLayerId(android.R.id.background);
+ background.setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN);
+ }
+
+ //触摸
+// public boolean onTouchEvent(MotionEvent event)
+
+
+ @Override
+ public boolean onDown(@NonNull MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public void onShowPress(@NonNull MotionEvent e) {
+
+ }
+
+ @Override
+ public boolean onSingleTapUp(@NonNull MotionEvent e) {
+ // 判断是哪个视图被点击了
+ if (!gj.isTablet(this)) {
+ switchViews(binding.cardview, binding.lrcView);
+ }
+ return true;
+ }
+
+ boolean isswitchViews = false;//是否在执行中
+
+ private void switchViews(final View view1, final View view2) {
+ // 隐藏view1并显示view2的动画效果
+ if (isswitchViews || gj.isTablet(this)) {
+ return;
+ }
+ isswitchViews = true;
+ if (view2.getId() == binding.lrcView.getId()) {
+ binding.lrcView.updateTime(player.getCurrentPosition(), true);
+ }
+ view1.animate()
+ .alpha(0.0f)
+ .setDuration(500)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view1.setVisibility(View.GONE);
+ view2.setVisibility(View.VISIBLE);
+ view2.setAlpha(0.0f);
+ view2.animate().alpha(1.0f).setDuration(500).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ isswitchViews = false;
+ }
+ });
+ }
+ });
+ }
+
+ // 判断触摸点是否在视图范围内的辅助方法
+ @Override
+ public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2,
+ float distanceX, float distanceY) {
+ float y = binding.getRoot().getRootView().getTranslationY() - distanceY;
+ y = Math.max(0, y);
+ //移动的距离
+ int heightPixels = getResources().getDisplayMetrics().heightPixels;
+ if (y > heightPixels - heightPixels / 5.0) {
+ finish();
+ return true;
+ }
+ binding.getRoot().getRootView().setTranslationY(y);
+ return true;
+ }
+
+ @Override
+ public void onLongPress(@NonNull MotionEvent e) {
+
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2,
+ float velocityX, float velocityY) {
+ return false;
+ }
+}
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..ad914e6
--- /dev/null
+++ b/app/src/main/java/com/muqingbfq/activity_about_software.java
@@ -0,0 +1,169 @@
+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.FragmentActivity;
+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 FragmentActivity {
+ @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 -> {
+ });
+ 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);
+ }
+ viewById.setText(Html.fromHtml(stringBuilder.toString(), Html.FROM_HTML_MODE_LEGACY));
+ open.close();
+ bufferedReader.close();
+
+ } catch (IOException e) {
+ 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