├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hyc │ │ └── dd_monitor │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── iconfont.ttf │ ├── java │ │ └── com │ │ │ └── hyc │ │ │ └── dd_monitor │ │ │ ├── MainActivity.kt │ │ │ ├── MainApplication.kt │ │ │ ├── models │ │ │ ├── PlayerOptions.kt │ │ │ └── UPInfo.kt │ │ │ ├── utils │ │ │ ├── CrashHandler.java │ │ │ ├── RecordingUtils.kt │ │ │ └── RoundImageTransform.kt │ │ │ └── views │ │ │ ├── DDLayout.kt │ │ │ ├── DDPlayer.kt │ │ │ ├── DanmuOptionsDialog.kt │ │ │ ├── GlobalVolumeDialog.kt │ │ │ ├── LayoutOptionsDialog.kt │ │ │ ├── ScanQrActivity.kt │ │ │ ├── UidImportDialog.kt │ │ │ └── VolumeControlDialog.kt │ └── res │ │ ├── drawable-night │ │ └── default_btn.xml │ │ ├── drawable │ │ ├── default_btn.xml │ │ ├── default_btn_night.xml │ │ ├── round.xml │ │ └── round_teal.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── dd_layout_1.xml │ │ ├── dd_layout_10.xml │ │ ├── dd_layout_11.xml │ │ ├── dd_layout_12.xml │ │ ├── dd_layout_13.xml │ │ ├── dd_layout_14.xml │ │ ├── dd_layout_15.xml │ │ ├── dd_layout_16.xml │ │ ├── dd_layout_17.xml │ │ ├── dd_layout_18.xml │ │ ├── dd_layout_19.xml │ │ ├── dd_layout_2.xml │ │ ├── dd_layout_20.xml │ │ ├── dd_layout_21.xml │ │ ├── dd_layout_22.xml │ │ ├── dd_layout_23.xml │ │ ├── dd_layout_24.xml │ │ ├── dd_layout_25.xml │ │ ├── dd_layout_26.xml │ │ ├── dd_layout_27.xml │ │ ├── dd_layout_3.xml │ │ ├── dd_layout_4.xml │ │ ├── dd_layout_5.xml │ │ ├── dd_layout_6.xml │ │ ├── dd_layout_7.xml │ │ ├── dd_layout_8.xml │ │ ├── dd_layout_9.xml │ │ ├── dd_player.xml │ │ ├── dialog_danmu_options.xml │ │ ├── dialog_global_volume.xml │ │ ├── dialog_layout_options.xml │ │ ├── dialog_scan_qr.xml │ │ ├── dialog_uidimport.xml │ │ ├── dialog_volume_single.xml │ │ ├── item_danmu_text.xml │ │ ├── item_layout_image.xml │ │ └── list_item_up.xml │ │ ├── menu │ │ ├── danmu_clear.xml │ │ ├── player_options.xml │ │ ├── player_options_more.xml │ │ ├── qn_menu.xml │ │ ├── timer_menu.xml │ │ └── up_item_card.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── layout1.png │ │ ├── layout10.png │ │ ├── layout11.png │ │ ├── layout12.png │ │ ├── layout13.png │ │ ├── layout14.png │ │ ├── layout15.png │ │ ├── layout16.png │ │ ├── layout17.png │ │ ├── layout18.png │ │ ├── layout19.png │ │ ├── layout2.png │ │ ├── layout20.png │ │ ├── layout21.png │ │ ├── layout22.png │ │ ├── layout23.png │ │ ├── layout24.png │ │ ├── layout25.png │ │ ├── layout26.png │ │ ├── layout27.png │ │ ├── layout3.png │ │ ├── layout4.png │ │ ├── layout5.png │ │ ├── layout6.png │ │ ├── layout7.png │ │ ├── layout8.png │ │ └── layout9.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-night │ │ ├── colors.xml │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── hyc │ └── dd_monitor │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── qrcode.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Josh Baker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DD_Monitor-android-kotlin 2 | 3 | ![GitHub all releases](https://img.shields.io/github/downloads/congHu/DD_Monitor-android-kotlin/total) 4 | 5 | ## 版本特性 6 | - 支持Android7.0以上。 7 | - 支持直播间id添加UP主。 8 | - 支持拖拽UP主卡片添加到直播窗口,支持窗口拖拽交换。 9 | - 支持横屏锁定。 10 | - 支持定时睡眠。 11 | - 支持16+11种布局方式。 12 | 13 | ## 使用须知 14 | - 使用时请注意宽带网速、流量消耗、电池电量、机身发热、系统卡顿等软硬件环境问题。 15 | - 本软件仅读取公开API数据,不涉及账号登录,欢迎查看源码进行监督。因此,本软件不支持弹幕互动、直播打赏等功能,若要使用请前往原版B站APP。 16 | - 直播流、UP主信息、以及个人公开的关注列表数据来自B站公开API,最终解释权归B站所有。 17 | 18 | ## 使用方式 19 | - 点击右上角“UP”按钮添加UP主,长按拖动到播放器窗口内。 20 | - 点击右上角"布局"按钮切换窗口布局。 21 | - 长按UP主卡片,然后拖动到窗口中。 22 | - 点击UP主卡片、或者点击下方UP主名字,弹出菜单选项,可跳转至UP主的直播间进行互动。 23 | 24 | ## TODO&BUG 25 | - [x] 音量的保存,以及交换窗口后同时交换音量大小 26 | - [x] 弹幕字体、视窗宽高 27 | - [x] 同传弹幕过滤 28 | - [x] 双击窗口全屏 29 | - [x] 前台开播提醒功能 30 | - [x] uid导入公开关注列表 31 | - [x] 显示SC弹幕 32 | - [x] 点击弹出删除弹幕选项(删除片哥) 33 | - [x] 扫码PC分享二维码 34 | 35 | ## 给开发者买杯奶茶 36 | [qrcode.png](qrcode.png) 37 | 38 | ## 开源依赖 39 | - [https://github.com/square/okhttp](https://github.com/square/okhttp) 40 | - [https://github.com/square/picasso](https://github.com/square/picasso) 41 | - [https://github.com/google/ExoPlayer](https://github.com/google/ExoPlayer) 42 | 43 | ## 相关链接 44 | - ios版:[https://gitee.com/hycong/dd-monitor-ios](https://gitee.com/hycong/dd-monitor-ios) 45 | - [https://gitee.com/zhimingshenjun/DD_Monitor_latest](https://gitee.com/zhimingshenjun/DD_Monitor_latest) 46 | - [https://github.com/lovelyyoshino/Bilibili-Live-API](https://github.com/lovelyyoshino/Bilibili-Live-API) 47 | - [https://github.com/MoyuScript/bilibili-api](https://github.com/MoyuScript/bilibili-api) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 29 8 | 9 | defaultConfig { 10 | applicationId "com.hyc.dd_monitor" 11 | minSdkVersion 24 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.1.21" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | dependencies { 35 | 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 37 | implementation 'androidx.core:core-ktx:1.3.2' 38 | implementation 'androidx.appcompat:appcompat:1.2.0' 39 | implementation 'com.google.android.material:material:1.2.1' 40 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 41 | 42 | implementation("com.squareup.okhttp3:okhttp:4.9.0") 43 | implementation 'com.google.android.exoplayer:exoplayer:2.13.2' 44 | implementation 'com.squareup.picasso:picasso:2.71828' 45 | // implementation 'com.h6ah4i.android.widget.verticalseekbar:verticalseekbar:1.0.0' 46 | implementation "com.google.code.gson:gson:2.8.6" 47 | implementation 'com.journeyapps:zxing-android-embedded:3.6.0' 48 | implementation("org.brotli:dec:0.1.2") 49 | 50 | testImplementation 'junit:junit:4.+' 51 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 52 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 53 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hyc/dd_monitor/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.hyc.dd_monitor 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.hyc.dd_monitor", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/assets/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/congHu/DD_Monitor-android-kotlin/b835fcba296c2d3fae90a9de081b514cdc5c16fc/app/src/main/assets/iconfont.ttf -------------------------------------------------------------------------------- /app/src/main/java/com/hyc/dd_monitor/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.hyc.dd_monitor 2 | 3 | import android.app.Application 4 | import com.hyc.dd_monitor.utils.CrashHandler 5 | 6 | class MainApplication : Application() { 7 | override fun onCreate() { 8 | super.onCreate() 9 | 10 | // 崩溃日志保存 11 | val crashHandler = CrashHandler.getInstance() 12 | crashHandler.init(this) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hyc/dd_monitor/models/PlayerOptions.kt: -------------------------------------------------------------------------------- 1 | package com.hyc.dd_monitor.models 2 | 3 | class PlayerOptions { 4 | var volume: Float = 1f 5 | var qn: Int = 80 6 | var isDanmuShow = true 7 | var danmuSize: Int = 13 8 | var danmuPosition: Int = 0 9 | var danmuWidth: Float = .2f 10 | var danmuHeight: Float = .8f 11 | var interpreterStyle: Int = 0 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hyc/dd_monitor/models/UPInfo.kt: -------------------------------------------------------------------------------- 1 | package com.hyc.dd_monitor.models 2 | 3 | class UPInfo { 4 | var faceImageUrl: String? = null 5 | var coverImageUrl: String? = null 6 | 7 | var uname: String? = null 8 | var title: String? = null 9 | 10 | var isLive: Boolean = false 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hyc/dd_monitor/utils/CrashHandler.java: -------------------------------------------------------------------------------- 1 | package com.hyc.dd_monitor.utils; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | import android.os.Build; 7 | import android.os.Environment; 8 | import android.util.Log; 9 | 10 | import java.io.BufferedWriter; 11 | import java.io.File; 12 | import java.io.FileWriter; 13 | import java.io.PrintWriter; 14 | import java.text.SimpleDateFormat; 15 | import java.util.Date; 16 | import java.util.concurrent.Executors; 17 | 18 | public class CrashHandler implements Thread.UncaughtExceptionHandler { 19 | 20 | private static final String TAG = "CrashHandler"; 21 | private static final boolean DEBUG = true; 22 | 23 | private static final String PATH = Environment.getExternalStorageDirectory().getPath() + "/DDPlayer/Logs/"; 24 | 25 | private static final String FILE_NAME = "crash"; 26 | private static final String FILE_NAME_SUFFIX = ".txt"; 27 | 28 | private static CrashHandler sInstance = new CrashHandler(); 29 | 30 | private Context mContext; 31 | 32 | //私有构造器 33 | private CrashHandler() { 34 | 35 | } 36 | 37 | //单例模式 38 | public static CrashHandler getInstance() { 39 | 40 | return sInstance; 41 | } 42 | 43 | //初始化 44 | public void init(Context context) { 45 | 46 | Thread.setDefaultUncaughtExceptionHandler(this); 47 | mContext = context; 48 | 49 | } 50 | 51 | /** 52 | * 重写uncaughtException 53 | * @param t 发生Crash的线程 54 | * @param ex Throwale对象 55 | */ 56 | @Override 57 | public void uncaughtException(Thread t, Throwable ex) { 58 | //处理逻辑需要开启一个子线程,用于文件的写入操作 59 | handleException(ex); 60 | //在程序关闭之前休眠2秒,以避免在文件写入的操作完 61 | //成。之前进程被杀死。 62 | //也可以考虑弹出对话框友好提示用户 63 | try { 64 | Thread.sleep(2000); 65 | } catch (InterruptedException e) { 66 | e.printStackTrace(); 67 | } 68 | 69 | System.exit(0); 70 | // Process.killProcess(Process.myPid()); 71 | } 72 | 73 | //处理异常 74 | private void handleException(final Throwable ex) { 75 | try { 76 | Executors.newSingleThreadExecutor().submit(new Runnable() { 77 | @Override 78 | public void run() { 79 | dumpExceptionToSDCard(ex); 80 | uploadExceptionToServer(); 81 | } 82 | }); 83 | } catch (Exception e) { 84 | e.printStackTrace(); 85 | } 86 | 87 | } 88 | 89 | /** 90 | * 将异常信息上传至服务器 91 | */ 92 | private void uploadExceptionToServer() { 93 | 94 | 95 | } 96 | 97 | /** 98 | * 将异常信息写入sd卡 99 | * @param ex 100 | */ 101 | private void dumpExceptionToSDCard(Throwable ex) { 102 | //判断是否支持SD卡 103 | if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 104 | if (DEBUG) { 105 | Log.i(TAG, "sdcard unfind ,skip dump exception"); 106 | return; 107 | } 108 | } 109 | 110 | File dir = new File(PATH); 111 | if (!dir.exists()) { 112 | dir.mkdirs(); 113 | } 114 | 115 | long current = System.currentTimeMillis(); 116 | String time = new SimpleDateFormat("yyyy-MM-dd").format(new Date(current)); 117 | 118 | File file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX); 119 | 120 | try { 121 | PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file))); 122 | 123 | pw.println(time); 124 | dumpPhoneInfo(pw); 125 | pw.println(); 126 | //将抛出的异常信息写入到文件 127 | pw.println(ex.getMessage()); 128 | ex.printStackTrace(pw); 129 | pw.close(); 130 | } catch (Exception e) { 131 | Log.d(TAG, "dump Exception Exception" + e.getMessage()); 132 | e.printStackTrace(); 133 | } 134 | 135 | } 136 | 137 | 138 | /** 139 | * 140 | * 获取手机信息 141 | * @param pw 写入流 142 | * @throws PackageManager.NameNotFoundException 异常 143 | */ 144 | private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException { 145 | 146 | PackageManager pm = mContext.getPackageManager(); 147 | PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES); 148 | 149 | pw.print("App Version : "); 150 | pw.print(pi.versionName); 151 | Log.d(TAG,"name : "+pi.versionName); 152 | pw.print('_'); 153 | pw.println(pi.versionCode); 154 | 155 | pw.print("OS Version : "); 156 | pw.print(Build.VERSION.RELEASE); 157 | pw.print("_"); 158 | pw.println(Build.VERSION.SDK_INT ); 159 | 160 | pw.print("Vendor : "); 161 | pw.println(Build.MANUFACTURER); 162 | 163 | pw.print("Model : "); 164 | pw.println(Build.MODEL); 165 | pw.print("Cpu ABI : "); 166 | pw.println(Build.CPU_ABI); 167 | } 168 | 169 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hyc/dd_monitor/utils/RecordingUtils.kt: -------------------------------------------------------------------------------- 1 | package com.hyc.dd_monitor.utils 2 | 3 | class RecordingUtils { 4 | companion object { 5 | fun byteString(len: Long) : String { 6 | if (len < 1024) return "${len}B" 7 | if (len < 1024*1024) return "${len/1024}K" 8 | if (len < 100*1024*1024) return String.format("%.1fM", len.toFloat()/1024f/1024f) 9 | return "${len/1024/1024}M" 10 | } 11 | 12 | fun minuteString(second: Long) : String { 13 | val min = second / 60 14 | val sec = second % 60 15 | val secStr = if (sec >= 10) "$sec" else "0$sec" 16 | return "$min:$secStr" 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hyc/dd_monitor/utils/RoundImageTransform.kt: -------------------------------------------------------------------------------- 1 | package com.hyc.dd_monitor.utils 2 | 3 | import android.graphics.* 4 | import com.squareup.picasso.Transformation 5 | 6 | class RoundImageTransform : Transformation { 7 | override fun transform(source: Bitmap?): Bitmap { 8 | val output = Bitmap.createBitmap(source!!.width, source.height, Bitmap.Config.ARGB_8888) 9 | 10 | val canvas = Canvas(output) 11 | val paint = Paint() 12 | paint.flags = Paint.ANTI_ALIAS_FLAG 13 | 14 | val rectF = RectF(Rect(0,0, output.width, output.height)) 15 | canvas.drawRoundRect(rectF, (output.width/2).toFloat(), 16 | (output.height/2).toFloat(), paint) 17 | 18 | val paintImg = Paint() 19 | paintImg.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP) 20 | canvas.drawBitmap(source, 0f, 0f, paintImg) 21 | source.recycle() 22 | 23 | return output 24 | } 25 | 26 | override fun key(): String { 27 | return "round" 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hyc/dd_monitor/views/DDLayout.kt: -------------------------------------------------------------------------------- 1 | package com.hyc.dd_monitor.views 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.LinearLayout 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.core.content.edit 10 | 11 | class DDLayout(context: Context?) : LinearLayout(context) { 12 | var layoutId : Int = 4 13 | set(value) { 14 | field = value 15 | reloadLayout() 16 | } 17 | 18 | var players: ArrayList 19 | 20 | var onCardDropListener: (() -> Unit)? = null 21 | 22 | var onPlayerClickListener: (() -> Unit)? = null 23 | 24 | var layoutPlayerCount = 0 25 | 26 | var fullScreenPlayerId: Int? = null 27 | var layoutBeforeFullScreen: Int? = null 28 | 29 | init { 30 | layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) 31 | // this.layoutId = 2 32 | 33 | 34 | this.players = ArrayList() 35 | 36 | // 开始就把最多9个对象准备好,交换窗口时无缝切换 37 | for (i in 0..8) { 38 | val p = DDPlayer(context!!, i) 39 | 40 | // 处理窗口交换 41 | p.onDragAndDropListener = { drag, drop -> 42 | Log.d("drop", "drag drop $drag $drop") 43 | val dragViewId = context.resources.getIdentifier( 44 | "dd_layout_${drag+1}", 45 | "id", 46 | context.packageName 47 | ) 48 | val dropViewId = context.resources.getIdentifier( 49 | "dd_layout_${drop+1}", 50 | "id", 51 | context.packageName 52 | ) 53 | val dragView = stackview?.findViewById(dragViewId) 54 | val dropView = stackview?.findViewById(dropViewId) 55 | (players[drop].parent as ViewGroup?)?.removeView(players[drop]) 56 | dragView?.addView(players[drop]) 57 | (players[drag].parent as ViewGroup?)?.removeView(players[drag]) 58 | dropView?.addView(players[drag]) 59 | players[drag].playerId = drop 60 | players[drop].playerId = drag 61 | 62 | val temp = players[drag] 63 | players[drag] = players[drop] 64 | players[drop] = temp 65 | 66 | // 在post里面重新获取宽度调整工具栏,因为post之后获取的才是正确的宽度 67 | post { 68 | players[drag].adjustControlBar() 69 | players[drop].adjustControlBar() 70 | } 71 | 72 | // 交换音量设置 73 | val volume2 = players[drop].playerOptions.volume 74 | players[drop].playerOptions.volume = players[drag].playerOptions.volume 75 | players[drop].notifyPlayerOptionsChange() 76 | players[drag].playerOptions.volume = volume2 77 | players[drag].notifyPlayerOptionsChange() 78 | 79 | context.getSharedPreferences("sp", AppCompatActivity.MODE_PRIVATE).edit { 80 | this.putString("roomId$drop", players[drop].roomId).apply() 81 | this.putString("roomId$drag", players[drag].roomId).apply() 82 | } 83 | } 84 | p.onDoubleClickListener = { 85 | if (layoutBeforeFullScreen == null) { 86 | val target = players[it] 87 | players[it] = players[0] 88 | players[0] = target 89 | fullScreenPlayerId = it 90 | layoutBeforeFullScreen = layoutId 91 | layoutId = 1 92 | }else{ 93 | val target = players[fullScreenPlayerId!!] 94 | players[fullScreenPlayerId!!] = players[0] 95 | players[0] = target 96 | layoutId = layoutBeforeFullScreen!! 97 | fullScreenPlayerId = null 98 | layoutBeforeFullScreen = null 99 | } 100 | 101 | 102 | } 103 | p.onCardDropListener = { 104 | onCardDropListener?.invoke() 105 | } 106 | p.onPlayerClickListener = { 107 | Log.d("ddclick", "ddclick") 108 | onPlayerClickListener?.invoke() 109 | } 110 | this.players.add(p) 111 | } 112 | 113 | // reloadLayout() 114 | 115 | context?.getSharedPreferences("sp", AppCompatActivity.MODE_PRIVATE)?.getInt("layout", 4)?.let { 116 | this.layoutId = it 117 | } 118 | } 119 | 120 | var stackview: View? = null 121 | 122 | // 重新刷新布局 123 | fun reloadLayout() { 124 | if (stackview != null) { 125 | removeView(stackview) 126 | } 127 | 128 | Log.d("ffffff", "dd_layout_$layoutId") 129 | val resId = context.resources.getIdentifier("dd_layout_$layoutId", "layout", context.packageName) 130 | stackview = inflate(context, resId, null) 131 | stackview?.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) 132 | addView(stackview) 133 | 134 | layoutPlayerCount = 0 135 | 136 | for (i in 1..9) { 137 | val layoutId = context.resources.getIdentifier("dd_layout_$i", "id", context.packageName) 138 | val v = stackview?.findViewById(layoutId) 139 | val p = players[i-1] 140 | (p.parent as ViewGroup?)?.removeView(p) 141 | v?.addView(p) 142 | 143 | if (v != null) { 144 | // if (fullScreenPlayerId != null && p.roomId != null) { // 判断是否双击全屏触发的 145 | // p.roomId = p.roomId 146 | // }else 147 | if (p.roomId == null) { 148 | p.roomId = context?.getSharedPreferences("sp", AppCompatActivity.MODE_PRIVATE)?.getString("roomId${p.playerId}", null) 149 | } 150 | 151 | post { 152 | p.adjustControlBar() 153 | } 154 | 155 | layoutPlayerCount += 1 156 | }else{ 157 | p.roomId = null 158 | } 159 | } 160 | 161 | } 162 | 163 | 164 | // 刷新全部 165 | fun refreshAll() { 166 | for (i in 0 until layoutPlayerCount) { 167 | val p = players[i] 168 | p.roomId = p.roomId 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hyc/dd_monitor/views/DanmuOptionsDialog.kt: -------------------------------------------------------------------------------- 1 | package com.hyc.dd_monitor.views 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Dialog 5 | import android.content.Context 6 | import android.os.Bundle 7 | import android.util.Log 8 | import android.widget.Button 9 | import android.widget.RadioGroup 10 | import android.widget.Switch 11 | import android.widget.TextView 12 | import androidx.appcompat.app.AppCompatActivity 13 | import com.google.gson.Gson 14 | import com.hyc.dd_monitor.R 15 | import com.hyc.dd_monitor.models.PlayerOptions 16 | import kotlin.math.roundToInt 17 | 18 | class DanmuOptionsDialog(context: Context, playerId: Int?) : Dialog(context) { 19 | 20 | var playerId: Int? = null 21 | 22 | var onDanmuOptionsChangeListener: ((options: PlayerOptions) -> Unit)? = null 23 | 24 | init { 25 | this.playerId = playerId 26 | } 27 | 28 | @SuppressLint("UseSwitchCompatOrMaterialCode") 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | setContentView(R.layout.dialog_danmu_options) 32 | 33 | val title = findViewById(R.id.danmu_options_title) 34 | 35 | var playerOptions = PlayerOptions() 36 | 37 | if (playerId != null) { 38 | title.text = "窗口#${playerId!! +1}弹幕设置" 39 | context.getSharedPreferences("sp", AppCompatActivity.MODE_PRIVATE).getString("opts$playerId", "")?.let { 40 | try { 41 | Log.d("playeroptions", "load $it") 42 | playerOptions = Gson().fromJson(it, PlayerOptions::class.java) 43 | }catch (e:java.lang.Exception) { 44 | 45 | } 46 | } 47 | }else{ 48 | title.text = "全局弹幕设置" 49 | } 50 | 51 | 52 | 53 | val switch = findViewById(R.id.isShow_switch) 54 | switch.isChecked = playerOptions.isDanmuShow 55 | switch.setOnCheckedChangeListener { compoundButton, b -> 56 | playerOptions.isDanmuShow = b 57 | onDanmuOptionsChangeListener?.invoke(playerOptions) 58 | } 59 | 60 | val pos = findViewById(R.id.danmu_position) 61 | 62 | when (playerOptions.danmuPosition) { 63 | 0 -> pos.check(R.id.danmu_position_lt) 64 | 1 -> pos.check(R.id.danmu_position_lb) 65 | 2 -> pos.check(R.id.danmu_position_rt) 66 | 3 -> pos.check(R.id.danmu_position_rb) 67 | } 68 | 69 | pos.setOnCheckedChangeListener { radioGroup, i -> 70 | when (i) { 71 | R.id.danmu_position_lt -> playerOptions.danmuPosition = 0 72 | R.id.danmu_position_lb -> playerOptions.danmuPosition = 1 73 | R.id.danmu_position_rt -> playerOptions.danmuPosition = 2 74 | R.id.danmu_position_rb -> playerOptions.danmuPosition = 3 75 | } 76 | onDanmuOptionsChangeListener?.invoke(playerOptions) 77 | } 78 | 79 | val fontsize = findViewById(R.id.fontsize_textview) 80 | fontsize.text = "${playerOptions.danmuSize}" 81 | findViewById