├── .gitignore ├── README.md ├── apk └── hanhan_video_player_v0.0.1_release_20230116.apk ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── lxr │ │ └── video_player │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── forward_speed.json │ ├── java │ │ └── com │ │ │ └── lxr │ │ │ └── video_player │ │ │ ├── MyApp.kt │ │ │ ├── action │ │ │ └── OnLongPressUpListener.kt │ │ │ ├── base │ │ │ ├── BaseActivity.kt │ │ │ └── BaseFragment.kt │ │ │ ├── constants │ │ │ ├── Constants.kt │ │ │ ├── MessageEvent.kt │ │ │ └── SimpleMessage.kt │ │ │ ├── entity │ │ │ ├── VideoFolder.kt │ │ │ └── VideoInfo.kt │ │ │ ├── receiver │ │ │ └── BatteryReceiver.kt │ │ │ ├── ui │ │ │ ├── LocalVideoListActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MovieFoldersListFragment.kt │ │ │ ├── PlayerActivity.kt │ │ │ ├── SettingFragment.kt │ │ │ └── SplashActivity.kt │ │ │ ├── utils │ │ │ ├── SpUtil.kt │ │ │ └── Utils.kt │ │ │ └── widget │ │ │ ├── CustomPlayer.kt │ │ │ ├── MyBatteryView.kt │ │ │ ├── SpaceItemDecoration.kt │ │ │ └── subtitle │ │ │ ├── GSYExoSubTitleModel.java │ │ │ ├── GSYExoSubTitlePlayer.java │ │ │ ├── GSYExoSubTitlePlayerManager.java │ │ │ └── GSYExoSubTitleVideoManager.java │ └── res │ │ ├── drawable-xxhdpi │ │ ├── ic_home.webp │ │ ├── ic_multi_choice.webp │ │ ├── ic_setting.webp │ │ ├── iv_splash.webp │ │ └── iv_video.webp │ │ ├── drawable │ │ ├── bg_progressbar.xml │ │ ├── bg_splash.xml │ │ ├── bottom_navigation_item_selector.xml │ │ ├── divider.xml │ │ ├── ic_round_pause_24.xml │ │ ├── ic_round_play_arrow_24.xml │ │ ├── ic_round_skip_next_24.xml │ │ ├── ic_round_skip_previous_24.xml │ │ └── ic_subtitles_24.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_player.xml │ │ ├── activity_simple_play.xml │ │ ├── activity_splash.xml │ │ ├── activity_video_list.xml │ │ ├── fragment_movie_folder_list.xml │ │ ├── fragment_setting.xml │ │ ├── item_folder.xml │ │ ├── item_play_history.xml │ │ ├── item_video.xml │ │ ├── video_layout_preview.xml │ │ └── widget_custom_video.xml │ │ ├── menu │ │ └── tabs_bottom.xml │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── lxr │ └── video_player │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hh.jks ├── screenshot ├── main.jpg ├── player.jpg ├── setting.jpg └── video_list.jpg └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hanhan_video_player 2 | 一款使用kotlin开发的本地视频播放器. 3 | 经常上下班在没网的地铁上看下好的电影,暂未发现适合自己的的播放器,写来自用,提供给需要的 4 | 基于[ GSYVideoPlayer](https://github.com/CarGuo/GSYVideoPlayer) 5 | [下载体验](https://www.pgyer.com/hanhan_video_player) 6 | 7 | ## 提示 8 | 项目代码为熟悉kotlin所编写,且未做优化,仅供参考 9 | 10 | 部分截图 11 | 12 | ![主页面](https://github.com/XiaoRanLiu3119/hanhan_video_player/blob/master/screenshot/main.jpg) 13 | ![视频列表](https://github.com/XiaoRanLiu3119/hanhan_video_player/blob/master/screenshot/video_list.jpg) 14 | ![播放](https://github.com/XiaoRanLiu3119/hanhan_video_player/blob/master/screenshot/player.jpg) 15 | 16 | 17 | ## 开发环境 18 | - Android Studio Chipmunk 2021.2.1 19 | - kotlin.android 1.7.10 20 | - gradle:7.3.3-bin 21 | - gradle plugin version:7.2.2 22 | ## 项目中用到的库,感谢大佬们 23 | - RecyclerView框架 [ BRV](https://github.com/liangjingkanji/BRV) 24 | - 权限 [ XXPermissions](https://github.com/getActivity/XXPermissions) 25 | - autosize 26 | - immersionbar 27 | - XPopup 28 | - ShadowLayout 29 | - TitleBar 30 | - Lottie 31 | - 更多详见app下的build.gradle 32 | ## 进度 33 | - [x] 基本使用 34 | - [x] 播放历史 35 | - [x] 长按倍速播放 36 | - [x] 切换播放器内核(ijk/exo/系统) 37 | - [x] 清除缓存 38 | - [x] 长按开启倍速反馈(震动和倍速图标显示) 39 | - [x] 多选操作 40 | - [ ] 自动跳过片头片尾 41 | - [ ] 优化ui(弹窗等) 42 | - [ ] 其余更多设置项 43 | -------------------------------------------------------------------------------- /apk/hanhan_video_player_v0.0.1_release_20230116.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/apk/hanhan_video_player_v0.0.1_release_20230116.apk -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | } 6 | 7 | android { 8 | signingConfigs { 9 | release { 10 | keyAlias = 'key0' 11 | storeFile file('..\\hh.jks') 12 | storePassword '123456' 13 | keyPassword '123456' 14 | } 15 | debug { 16 | keyAlias = 'key0' 17 | storeFile file('..\\hh.jks') 18 | storePassword '123456' 19 | keyPassword '123456' 20 | } 21 | } 22 | 23 | compileSdk 32 24 | defaultConfig { 25 | applicationId "com.lxr.video_player" 26 | minSdk 24 27 | targetSdk 32 28 | versionCode 2 29 | versionName "0.0.2" 30 | 31 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 32 | 33 | ndk { 34 | //设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so) 35 | abiFilters "arm64-v8a" //,"armeabi-v7a" 36 | } 37 | } 38 | 39 | // 执行配置 40 | applicationVariants.all { variant -> 41 | // Apk 输出配置 42 | variant.outputs.all { output -> 43 | outputFileName = rootProject.getName() + '_v' + variant.versionName + '_' + variant.buildType.name 44 | if (variant.buildType.name == buildTypes.release.getName()) { 45 | outputFileName += '_' + new Date().format("yyyyMMdd") 46 | } 47 | outputFileName += '.apk' 48 | } 49 | } 50 | 51 | buildTypes { 52 | release { 53 | minifyEnabled false 54 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 55 | } 56 | } 57 | compileOptions { 58 | sourceCompatibility JavaVersion.VERSION_1_8 59 | targetCompatibility JavaVersion.VERSION_1_8 60 | } 61 | kotlinOptions { 62 | jvmTarget = '1.8' 63 | } 64 | 65 | buildFeatures { 66 | viewBinding true 67 | dataBinding true 68 | } 69 | } 70 | 71 | dependencies { 72 | 73 | implementation 'androidx.core:core-ktx:1.7.0' 74 | implementation 'androidx.appcompat:appcompat:1.2.0' 75 | implementation 'com.google.android.material:material:1.7.0' 76 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 77 | testImplementation 'junit:junit:4.13.2' 78 | androidTestImplementation 'androidx.test.ext:junit:1.1.4' 79 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' 80 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 81 | implementation 'com.google.android.material:material:1.4.0' 82 | def lifecycle_version = "2.5.0" 83 | implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" 84 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 85 | 86 | //权限 87 | implementation 'com.github.getActivity:XXPermissions:16.6' 88 | //图片加载 89 | implementation 'com.github.bumptech.glide:glide:4.12.0' 90 | annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' 91 | //recyclerview适配器框架 92 | implementation 'com.github.liangjingkanji:BRV:1.3.88' 93 | //谷歌刷新头 94 | implementation 'io.github.scwang90:refresh-header-material:2.0.5' 95 | //mmkv 96 | implementation 'com.tencent:mmkv:1.0.19' 97 | //屏幕适配AutoSize 98 | implementation 'me.jessyan:autosize:1.2.1' 99 | //util 100 | implementation 'com.blankj:utilcodex:1.31.0' 101 | //沉浸式基础依赖包,必须要依赖 102 | implementation 'com.geyifeng.immersionbar:immersionbar:3.2.2' 103 | //沉浸式kotlin扩展(可选) 104 | implementation 'com.geyifeng.immersionbar:immersionbar-ktx:3.2.2' 105 | //弹窗 106 | implementation 'com.github.li-xiaojun:XPopup:2.8.2' 107 | //阴影.圆角.动态设置shape和selector 108 | implementation 'com.github.lihangleo2:ShadowLayout:3.3.2' 109 | //一个可定制且易于使用的BottomBar导航视图,带有流畅的动画,支持ViewPager、ViewPager2、NavController和徽章。 110 | implementation 'nl.joery.animatedbottombar:library:1.1.0' 111 | //标题栏 112 | implementation 'com.github.getActivity:TitleBar:10.0' 113 | //eventbus消息 114 | implementation 'org.greenrobot:eventbus:3.1.1' 115 | 116 | //播放器 117 | implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.3.4-release-jitpack' 118 | implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.3.4-release-jitpack' 119 | //根据你的需求ijk模式的so 120 | implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-arm64:v8.3.4-release-jitpack' 121 | implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-armv7a:v8.3.4-release-jitpack' 122 | //lottie动画 123 | implementation 'com.airbnb.android:lottie:5.2.0' 124 | //选择器 125 | implementation 'com.github.gzu-liyujiang.AndroidPicker:Common:4.1.11' 126 | //文件选择器 127 | implementation 'com.github.gzu-liyujiang.AndroidPicker:FilePicker:4.1.11' 128 | } -------------------------------------------------------------------------------- /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/lxr/video_player/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player 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.lxr.video_player", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 25 | 26 | 31 | 35 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 55 | 58 | 59 | 60 | 63 | 64 | 65 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/assets/forward_speed.json: -------------------------------------------------------------------------------- 1 | {"v":"5.5.1","fr":29.9700012207031,"ip":0,"op":91.000003706506,"w":800,"h":800,"nm":"Fast Forward","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":4,"ty":4,"nm":"Fast forward Outlines 13","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[100]},{"t":90.0000036657751,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[406,403,0],"to":[17.5,0,0],"ti":[-17.5,0,0]},{"t":90.0000036657751,"s":[511,403,0]}],"ix":2},"a":{"a":0,"k":[100.131,95.213,0],"ix":1},"s":{"a":0,"k":[122,122,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.43,-1.685],[0,0],[-0.133,-2.358],[2.359,-1.749],[0,0],[0,3.032],[0,0],[0,0],[0,3.031],[0,0],[-2.497,-1.751],[0,0],[0,0]],"o":[[0,0],[2.023,1.416],[0.07,2.091],[0,0],[-2.427,1.687],[0,0],[0,0],[-2.428,1.756],[0,0],[0,-3.101],[0,0],[0,0],[0,-3.033]],"v":[[-1.788,-31.574],[34.832,-5.9],[40.091,-0.374],[34.832,5.084],[-1.926,31.57],[-7.856,28.674],[-7.927,13.441],[-33.957,32.243],[-40.027,29.346],[-40.161,-29.079],[-33.823,-32.248],[-7.996,-14.12],[-7.996,-28.474]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[104.62,95.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":71.0000028918893,"op":91.000003706506,"st":72.0000029326201,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Fast forward Outlines 11","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":54,"s":[100]},{"t":72.0000029326201,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":54,"s":[406,403,0],"to":[17.5,0,0],"ti":[-17.5,0,0]},{"t":72.0000029326201,"s":[511,403,0]}],"ix":2},"a":{"a":0,"k":[100.131,95.213,0],"ix":1},"s":{"a":0,"k":[122,122,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.43,-1.685],[0,0],[-0.133,-2.358],[2.359,-1.749],[0,0],[0,3.032],[0,0],[0,0],[0,3.031],[0,0],[-2.497,-1.751],[0,0],[0,0]],"o":[[0,0],[2.023,1.416],[0.07,2.091],[0,0],[-2.427,1.687],[0,0],[0,0],[-2.428,1.756],[0,0],[0,-3.101],[0,0],[0,0],[0,-3.033]],"v":[[-1.788,-31.574],[34.832,-5.9],[40.091,-0.374],[34.832,5.084],[-1.926,31.57],[-7.856,28.674],[-7.927,13.441],[-33.957,32.243],[-40.027,29.346],[-40.161,-29.079],[-33.823,-32.248],[-7.996,-14.12],[-7.996,-28.474]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[104.62,95.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":53.0000021587343,"op":73.0000029733509,"st":54.0000021994651,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Fast forward Outlines 9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":36,"s":[100]},{"t":54.0000021994651,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[406,403,0],"to":[17.5,0,0],"ti":[-17.5,0,0]},{"t":54.0000021994651,"s":[511,403,0]}],"ix":2},"a":{"a":0,"k":[100.131,95.213,0],"ix":1},"s":{"a":0,"k":[122,122,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.43,-1.685],[0,0],[-0.133,-2.358],[2.359,-1.749],[0,0],[0,3.032],[0,0],[0,0],[0,3.031],[0,0],[-2.497,-1.751],[0,0],[0,0]],"o":[[0,0],[2.023,1.416],[0.07,2.091],[0,0],[-2.427,1.687],[0,0],[0,0],[-2.428,1.756],[0,0],[0,-3.101],[0,0],[0,0],[0,-3.033]],"v":[[-1.788,-31.574],[34.832,-5.9],[40.091,-0.374],[34.832,5.084],[-1.926,31.57],[-7.856,28.674],[-7.927,13.441],[-33.957,32.243],[-40.027,29.346],[-40.161,-29.079],[-33.823,-32.248],[-7.996,-14.12],[-7.996,-28.474]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[104.62,95.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":35.0000014255792,"op":55.0000022401959,"st":36.0000014663101,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Fast forward Outlines 7","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":18,"s":[100]},{"t":36.0000014663101,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[406,403,0],"to":[17.5,0,0],"ti":[-17.5,0,0]},{"t":36.0000014663101,"s":[511,403,0]}],"ix":2},"a":{"a":0,"k":[100.131,95.213,0],"ix":1},"s":{"a":0,"k":[122,122,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.43,-1.685],[0,0],[-0.133,-2.358],[2.359,-1.749],[0,0],[0,3.032],[0,0],[0,0],[0,3.031],[0,0],[-2.497,-1.751],[0,0],[0,0]],"o":[[0,0],[2.023,1.416],[0.07,2.091],[0,0],[-2.427,1.687],[0,0],[0,0],[-2.428,1.756],[0,0],[0,-3.101],[0,0],[0,0],[0,-3.033]],"v":[[-1.788,-31.574],[34.832,-5.9],[40.091,-0.374],[34.832,5.084],[-1.926,31.57],[-7.856,28.674],[-7.927,13.441],[-33.957,32.243],[-40.027,29.346],[-40.161,-29.079],[-33.823,-32.248],[-7.996,-14.12],[-7.996,-28.474]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[104.62,95.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":17.0000006924242,"op":37.0000015070409,"st":18.000000733155,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Fast forward Outlines 5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":18.000000733155,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[406,403,0],"to":[17.5,0,0],"ti":[-17.5,0,0]},{"t":18.000000733155,"s":[511,403,0]}],"ix":2},"a":{"a":0,"k":[100.131,95.213,0],"ix":1},"s":{"a":0,"k":[122,122,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.43,-1.685],[0,0],[-0.133,-2.358],[2.359,-1.749],[0,0],[0,3.032],[0,0],[0,0],[0,3.031],[0,0],[-2.497,-1.751],[0,0],[0,0]],"o":[[0,0],[2.023,1.416],[0.07,2.091],[0,0],[-2.427,1.687],[0,0],[0,0],[-2.428,1.756],[0,0],[0,-3.101],[0,0],[0,0],[0,-3.033]],"v":[[-1.788,-31.574],[34.832,-5.9],[40.091,-0.374],[34.832,5.084],[-1.926,31.57],[-7.856,28.674],[-7.927,13.441],[-33.957,32.243],[-40.027,29.346],[-40.161,-29.079],[-33.823,-32.248],[-7.996,-14.12],[-7.996,-28.474]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[104.62,95.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":19.0000007738859,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Fast forward Outlines 12","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[0]},{"t":90.0000036657751,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[302,403,0],"to":[17.333,0,0],"ti":[-17.333,0,0]},{"t":90.0000036657751,"s":[406,403,0]}],"ix":2},"a":{"a":0,"k":[100.131,95.213,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":72,"s":[81,81,100]},{"t":90.0000036657751,"s":[122,122,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.43,-1.685],[0,0],[-0.133,-2.358],[2.359,-1.749],[0,0],[0,3.032],[0,0],[0,0],[0,3.031],[0,0],[-2.497,-1.751],[0,0],[0,0]],"o":[[0,0],[2.023,1.416],[0.07,2.091],[0,0],[-2.427,1.687],[0,0],[0,0],[-2.428,1.756],[0,0],[0,-3.101],[0,0],[0,0],[0,-3.033]],"v":[[-1.788,-31.574],[34.832,-5.9],[40.091,-0.374],[34.832,5.084],[-1.926,31.57],[-7.856,28.674],[-7.927,13.441],[-33.957,32.243],[-40.027,29.346],[-40.161,-29.079],[-33.823,-32.248],[-7.996,-14.12],[-7.996,-28.474]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[104.62,95.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":71.0000028918893,"op":91.000003706506,"st":72.0000029326201,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Fast forward Outlines 10","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":54,"s":[0]},{"t":72.0000029326201,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":54,"s":[302,403,0],"to":[17.333,0,0],"ti":[-17.333,0,0]},{"t":72.0000029326201,"s":[406,403,0]}],"ix":2},"a":{"a":0,"k":[100.131,95.213,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":54,"s":[81,81,100]},{"t":72.0000029326201,"s":[122,122,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.43,-1.685],[0,0],[-0.133,-2.358],[2.359,-1.749],[0,0],[0,3.032],[0,0],[0,0],[0,3.031],[0,0],[-2.497,-1.751],[0,0],[0,0]],"o":[[0,0],[2.023,1.416],[0.07,2.091],[0,0],[-2.427,1.687],[0,0],[0,0],[-2.428,1.756],[0,0],[0,-3.101],[0,0],[0,0],[0,-3.033]],"v":[[-1.788,-31.574],[34.832,-5.9],[40.091,-0.374],[34.832,5.084],[-1.926,31.57],[-7.856,28.674],[-7.927,13.441],[-33.957,32.243],[-40.027,29.346],[-40.161,-29.079],[-33.823,-32.248],[-7.996,-14.12],[-7.996,-28.474]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[104.62,95.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":53.0000021587343,"op":73.0000029733509,"st":54.0000021994651,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Fast forward Outlines 8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":36,"s":[0]},{"t":54.0000021994651,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[302,403,0],"to":[17.333,0,0],"ti":[-17.333,0,0]},{"t":54.0000021994651,"s":[406,403,0]}],"ix":2},"a":{"a":0,"k":[100.131,95.213,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":36,"s":[81,81,100]},{"t":54.0000021994651,"s":[122,122,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.43,-1.685],[0,0],[-0.133,-2.358],[2.359,-1.749],[0,0],[0,3.032],[0,0],[0,0],[0,3.031],[0,0],[-2.497,-1.751],[0,0],[0,0]],"o":[[0,0],[2.023,1.416],[0.07,2.091],[0,0],[-2.427,1.687],[0,0],[0,0],[-2.428,1.756],[0,0],[0,-3.101],[0,0],[0,0],[0,-3.033]],"v":[[-1.788,-31.574],[34.832,-5.9],[40.091,-0.374],[34.832,5.084],[-1.926,31.57],[-7.856,28.674],[-7.927,13.441],[-33.957,32.243],[-40.027,29.346],[-40.161,-29.079],[-33.823,-32.248],[-7.996,-14.12],[-7.996,-28.474]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[104.62,95.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":35.0000014255792,"op":55.0000022401959,"st":36.0000014663101,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Fast forward Outlines 6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":18,"s":[0]},{"t":36.0000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[302,403,0],"to":[17.333,0,0],"ti":[-17.333,0,0]},{"t":36.0000014663101,"s":[406,403,0]}],"ix":2},"a":{"a":0,"k":[100.131,95.213,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":18,"s":[81,81,100]},{"t":36.0000014663101,"s":[122,122,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.43,-1.685],[0,0],[-0.133,-2.358],[2.359,-1.749],[0,0],[0,3.032],[0,0],[0,0],[0,3.031],[0,0],[-2.497,-1.751],[0,0],[0,0]],"o":[[0,0],[2.023,1.416],[0.07,2.091],[0,0],[-2.427,1.687],[0,0],[0,0],[-2.428,1.756],[0,0],[0,-3.101],[0,0],[0,0],[0,-3.033]],"v":[[-1.788,-31.574],[34.832,-5.9],[40.091,-0.374],[34.832,5.084],[-1.926,31.57],[-7.856,28.674],[-7.927,13.441],[-33.957,32.243],[-40.027,29.346],[-40.161,-29.079],[-33.823,-32.248],[-7.996,-14.12],[-7.996,-28.474]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[104.62,95.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":17.0000006924242,"op":37.0000015070409,"st":18.000000733155,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Fast forward Outlines 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":18.000000733155,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[302,403,0],"to":[17.333,0,0],"ti":[-17.333,0,0]},{"t":18.000000733155,"s":[406,403,0]}],"ix":2},"a":{"a":0,"k":[100.131,95.213,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[81,81,100]},{"t":18.000000733155,"s":[122,122,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.43,-1.685],[0,0],[-0.133,-2.358],[2.359,-1.749],[0,0],[0,3.032],[0,0],[0,0],[0,3.031],[0,0],[-2.497,-1.751],[0,0],[0,0]],"o":[[0,0],[2.023,1.416],[0.07,2.091],[0,0],[-2.427,1.687],[0,0],[0,0],[-2.428,1.756],[0,0],[0,-3.101],[0,0],[0,0],[0,-3.033]],"v":[[-1.788,-31.574],[34.832,-5.9],[40.091,-0.374],[34.832,5.084],[-1.926,31.57],[-7.856,28.674],[-7.927,13.441],[-33.957,32.243],[-40.027,29.346],[-40.161,-29.079],[-33.823,-32.248],[-7.996,-14.12],[-7.996,-28.474]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[104.62,95.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":19.0000007738859,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/MyApp.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player 2 | 3 | import android.app.Application 4 | import com.drake.brv.utils.BRV 5 | import com.lxj.xpopup.XPopup 6 | import com.lxr.video_player.constants.Constants 7 | import com.lxr.video_player.utils.SpUtil 8 | import com.scwang.smart.refresh.header.MaterialHeader 9 | import com.scwang.smart.refresh.layout.SmartRefreshLayout 10 | import com.tencent.mmkv.MMKV 11 | import me.jessyan.autosize.AutoSizeConfig 12 | 13 | /** 14 | * @Author : Liu XiaoRan 15 | * @Email : 592923276@qq.com 16 | * @Date : on 2023/1/9 09:30. 17 | * @Description : 18 | */ 19 | class MyApp : Application() { 20 | companion object { 21 | lateinit var instance: MyApp 22 | } 23 | 24 | override fun onCreate() { 25 | super.onCreate() 26 | instance = this 27 | MMKV.initialize(this) 28 | SmartRefreshLayout.setDefaultRefreshHeaderCreator { _, _ -> MaterialHeader(this) } 29 | // brv使用,初始化BindingAdapter的默认绑定ID, 如果不使用DataBinding并不需要初始化 30 | BRV.modelId = BR.m 31 | // 不想让 App 内的字体大小跟随系统设置中字体大小的改变 32 | AutoSizeConfig.getInstance().isExcludeFontScale = true 33 | XPopup.setPrimaryColor(R.color.colorPrimary) 34 | initCacheConfig() 35 | } 36 | 37 | private fun initCacheConfig() { 38 | if (SpUtil.getString(Constants.K_DEFAULT_PATH_4_FIND_SUBTITLE).isNullOrEmpty()) { 39 | SpUtil.put(Constants.K_DEFAULT_PATH_4_FIND_SUBTITLE, Constants.V_DEFAULT_PATH_4_FIND_SUBTITLE) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/action/OnLongPressUpListener.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.action 2 | 3 | /** 4 | * @Author : Liu XiaoRan 5 | * @Email : 592923276@qq.com 6 | * @Date : on 2023/1/12 15:35. 7 | * @Description : 长按抬起监听 8 | */ 9 | interface OnLongPressUpListener { 10 | /** 11 | * start 回调 true 开始 回调 false 结束 12 | */ 13 | fun onLongPressIsStart(start :Boolean) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dyne.myktdemo.base 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.view.Window 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.viewbinding.ViewBinding 10 | import com.blankj.utilcode.util.LogUtils 11 | import com.gyf.immersionbar.ktx.fitsTitleBar 12 | import com.gyf.immersionbar.ktx.immersionBar 13 | import com.hjq.bar.OnTitleBarListener 14 | import com.hjq.bar.TitleBar 15 | import com.lxj.xpopup.XPopup 16 | import com.lxj.xpopup.impl.LoadingPopupView 17 | import com.lxr.video_player.constants.MessageEvent 18 | import org.greenrobot.eventbus.EventBus 19 | import org.greenrobot.eventbus.Subscribe 20 | import org.greenrobot.eventbus.ThreadMode 21 | import java.lang.reflect.ParameterizedType 22 | 23 | abstract class BaseActivity : AppCompatActivity(), OnTitleBarListener { 24 | val TAG = javaClass.name 25 | protected lateinit var binding: T 26 | 27 | protected var loadingPopup: LoadingPopupView? = null 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | initBeforeInitView() 32 | val type = javaClass.genericSuperclass as ParameterizedType 33 | val aClass = type.actualTypeArguments[0] as Class<*> 34 | val method = aClass.getDeclaredMethod("inflate", LayoutInflater::class.java) 35 | binding = method.invoke(null, layoutInflater) as T 36 | setContentView(binding.root) 37 | EventBus.getDefault().register(this) 38 | 39 | initTitleStatusBar() 40 | initView() 41 | initListener() 42 | } 43 | 44 | override fun onDestroy() { 45 | super.onDestroy() 46 | EventBus.getDefault().unregister(this) 47 | } 48 | 49 | @Subscribe(threadMode = ThreadMode.MAIN) 50 | open fun onSimpleMessage(simpleMessage: String?) { 51 | } 52 | 53 | @Subscribe(threadMode = ThreadMode.MAIN) 54 | open fun onEvent(event: MessageEvent) { 55 | 56 | } 57 | 58 | /** 59 | * 布局初始化之前 60 | */ 61 | protected open fun initBeforeInitView(){ 62 | 63 | } 64 | 65 | /** 66 | * 初始化布局 67 | */ 68 | abstract fun initView() 69 | 70 | /** 71 | * 初始化监听器 72 | */ 73 | open fun initListener() { 74 | 75 | } 76 | 77 | /** 78 | * 初始化标题栏和沉浸式状态栏 79 | */ 80 | private fun initTitleStatusBar(){ 81 | val findTitleBar = findTitleBar(findViewById(Window.ID_ANDROID_CONTENT)) 82 | findTitleBar?.setOnTitleBarListener(this) 83 | 84 | immersionBar { 85 | if (findTitleBar != null) { 86 | fitsTitleBar(findTitleBar) 87 | } 88 | } 89 | } 90 | 91 | 92 | override fun onLeftClick(titleBar: TitleBar?) { 93 | super.onLeftClick(titleBar) 94 | onBackPressed() 95 | } 96 | 97 | override fun onRightClick(titleBar: TitleBar?) { 98 | super.onRightClick(titleBar) 99 | } 100 | 101 | override fun onTitleClick(titleBar: TitleBar?) { 102 | super.onTitleClick(titleBar) 103 | } 104 | 105 | private fun findTitleBar(group: ViewGroup): TitleBar? { 106 | for (i in 0 until group.childCount) { 107 | val view = group.getChildAt(i) 108 | if (view is TitleBar) { 109 | return view 110 | } else if (view is ViewGroup) { 111 | val titleBar = findTitleBar(view) 112 | if (titleBar != null) { 113 | return titleBar 114 | } 115 | } 116 | } 117 | return null 118 | } 119 | 120 | /** 121 | * 打开等待框 122 | */ 123 | protected fun showLoading() { 124 | if (loadingPopup == null) { 125 | loadingPopup = XPopup.Builder(this) 126 | .dismissOnBackPressed(false) 127 | .isLightNavigationBar(true) 128 | .hasShadowBg(false) 129 | .asLoading() 130 | .show() as LoadingPopupView 131 | } 132 | loadingPopup?.show() 133 | } 134 | 135 | /** 136 | * 关闭等待框 137 | */ 138 | protected fun dismissLoading() { 139 | loadingPopup?.dismiss() 140 | } 141 | 142 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dyne.myktdemo.base 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.appcompat.widget.Toolbar 8 | import androidx.fragment.app.Fragment 9 | import androidx.viewbinding.ViewBinding 10 | import com.blankj.utilcode.util.LogUtils 11 | import com.gyf.immersionbar.ktx.fitsTitleBar 12 | import com.gyf.immersionbar.ktx.immersionBar 13 | import com.hjq.bar.OnTitleBarListener 14 | import com.hjq.bar.TitleBar 15 | import com.lxj.xpopup.XPopup 16 | import com.lxj.xpopup.impl.LoadingPopupView 17 | import org.greenrobot.eventbus.EventBus 18 | import org.greenrobot.eventbus.Subscribe 19 | import org.greenrobot.eventbus.ThreadMode 20 | import java.lang.reflect.ParameterizedType 21 | 22 | /** 23 | * @Author : xia chuanqi 24 | * @Email : 751528989@qq.com 25 | * @Date : on 2022/6/9 17:08. 26 | * @Description : 27 | */ 28 | abstract class BaseFragment : Fragment(),OnTitleBarListener{ 29 | val TAG = javaClass.name 30 | protected lateinit var binding: T 31 | protected var loadingPopup: LoadingPopupView? = null 32 | 33 | override fun onCreateView( 34 | inflater: LayoutInflater, 35 | container: ViewGroup?, 36 | savedInstanceState: Bundle? 37 | ): View { 38 | val type = javaClass.genericSuperclass as ParameterizedType 39 | val aClass = type.actualTypeArguments[0] as Class<*> 40 | val method = aClass.getDeclaredMethod("inflate", LayoutInflater::class.java,ViewGroup::class.java,Boolean::class.java) 41 | binding = method.invoke(null,layoutInflater,container,false) as T 42 | return binding.root 43 | } 44 | 45 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 46 | super.onViewCreated(view, savedInstanceState) 47 | initView() 48 | initTitleStatusBar() 49 | initData() 50 | initListener() 51 | } 52 | 53 | override fun onCreate(savedInstanceState: Bundle?) { 54 | super.onCreate(savedInstanceState) 55 | EventBus.getDefault().register(this) 56 | } 57 | 58 | override fun onDestroy() { 59 | super.onDestroy() 60 | EventBus.getDefault().unregister(this) 61 | } 62 | 63 | @Subscribe(threadMode = ThreadMode.MAIN) 64 | open fun onEvent(simpleMessage: String?) { 65 | } 66 | 67 | /** 68 | * 初始化布局 69 | */ 70 | open fun initView(){ 71 | 72 | } 73 | 74 | /** 75 | * 页面初始化后初始化数据 76 | */ 77 | open fun initData() { 78 | 79 | } 80 | 81 | /** 82 | * 初始化监听器 83 | */ 84 | open fun initListener() { 85 | 86 | } 87 | 88 | 89 | /** 90 | * 打开等待框 91 | */ 92 | protected fun showLoading() { 93 | if (loadingPopup == null) { 94 | loadingPopup = XPopup.Builder(context) 95 | .dismissOnBackPressed(false) 96 | .isLightNavigationBar(true) 97 | .hasShadowBg(false) 98 | .asLoading() 99 | .show() as LoadingPopupView 100 | } 101 | loadingPopup?.show() 102 | } 103 | 104 | /** 105 | * 关闭等待框 106 | */ 107 | protected fun dismissLoading() { 108 | loadingPopup?.dismiss() 109 | } 110 | 111 | 112 | private fun initTitleStatusBar(){ 113 | val findTitleBar = findTitleBar((view as ViewGroup?)!!) 114 | if (findTitleBar is TitleBar){ 115 | findTitleBar.setOnTitleBarListener(this) 116 | } 117 | 118 | immersionBar { 119 | if (findTitleBar != null) {//沉浸式适配出标题栏的颜色 120 | fitsTitleBar(findTitleBar) 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * 遍历出标题栏 127 | */ 128 | private fun findTitleBar(group: ViewGroup): View? { 129 | for (i in 0 until group.childCount) { 130 | val view = group.getChildAt(i) 131 | if (view is TitleBar || view is Toolbar) { 132 | return view 133 | } else if (view is ViewGroup) { 134 | val titleBar = findTitleBar(view) 135 | if (titleBar != null) { 136 | return titleBar 137 | } 138 | } 139 | } 140 | return null 141 | } 142 | 143 | override fun onLeftClick(titleBar: TitleBar?) { 144 | super.onLeftClick(titleBar) 145 | } 146 | 147 | override fun onRightClick(titleBar: TitleBar?) { 148 | super.onRightClick(titleBar) 149 | } 150 | 151 | override fun onTitleClick(titleBar: TitleBar?) { 152 | super.onTitleClick(titleBar) 153 | } 154 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/constants/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.constants 2 | 3 | import com.drake.brv.listener.ItemDifferCallback 4 | 5 | /** 6 | * @Author : Liu XiaoRan 7 | * @Email : 592923276@qq.com 8 | * @Date : on 2023/1/13 09:24. 9 | * @Description : 10 | */ 11 | object Constants { 12 | 13 | /** 14 | * 消息类型 电量 15 | */ 16 | const val MSG_TYPE_BATTERY = "msg_type_battery" 17 | 18 | /** 19 | * 支持(筛选)的字幕类型 20 | */ 21 | @JvmField 22 | val SUPPORT_SUBTITLE_TYPE = arrayOf(".srt", ".ass", ".ssa") 23 | 24 | /** 25 | * 查找字幕文件的默认路径key 26 | */ 27 | const val K_DEFAULT_PATH_4_FIND_SUBTITLE = "default_path_4_find_subtitle" 28 | 29 | /** 30 | * 查找字幕文件的默认路径value 31 | */ 32 | const val V_DEFAULT_PATH_4_FIND_SUBTITLE = "sdcard" 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/constants/MessageEvent.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.constants 2 | 3 | /** 4 | * @Author : Liu XiaoRan 5 | * @Email : 592923276@qq.com 6 | * @Date : on 2023/1/29 11:49. 7 | * @Description : 8 | */ 9 | data class MessageEvent(var type:String,var message:String = "") 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/constants/SimpleMessage.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.constants 2 | 3 | /** 4 | * @Author : Liu XiaoRan 5 | * @Email : 592923276@qq.com 6 | * @Date : on 2023/1/13 09:43. 7 | * @Description : 8 | */ 9 | object SimpleMessage { 10 | /** 11 | * 全局通用的刷新 12 | */ 13 | const val REFRESH = "refresh" 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/entity/VideoFolder.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.entity 2 | 3 | /** 4 | * @Author : Liu XiaoRan 5 | * @Email : 592923276@qq.com 6 | * @Date : on 2023/1/11 15:41. 7 | * @Description :装载视频的文件夹对象 8 | */ 9 | data class VideoFolder(var name:String? = "", var videoList: MutableList) -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/entity/VideoInfo.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.entity 2 | 3 | import android.graphics.Bitmap 4 | import androidx.databinding.BaseObservable 5 | 6 | /** 7 | * @Author : Liu XiaoRan 8 | * @Email : 592923276@qq.com 9 | * @Date : on 2023/1/9 09:42. 10 | * @Description : 视频信息 11 | */ 12 | data class VideoInfo( 13 | var checked: Boolean = false, 14 | var checkBoxVisibility: Boolean = false,//选择框的可见性,跟随列表的编辑模式开关 15 | 16 | var id :Int = 0,//视频id 17 | var path: String? = null,//文件路径 18 | var size: Long = 0,//大小 19 | var displayName: String? = null,//视频名字,不带后缀 20 | var title: String? = null,//视频标题,带后缀 21 | var duration: Long = 0,//时长,部分视频损坏/其他原因没有 22 | var resolution: String? = null,//分辨率 23 | var isPrivate:Int = 0,//私密? 24 | var bucketId: String? = null,//装载(文件夹)id 25 | var bucketDisplayName: String? = null,//装载文件夹名字 26 | var thumbnail: Bitmap? = null,//缩略图 27 | var bookmark: String? = null,//书签,上次播放的位置 28 | 29 | ) : BaseObservable() // BaseObservable 这是DataBinding的数据绑定写法,数据变化通知ui 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/receiver/BatteryReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.lxr.video_player.constants.Constants 7 | import com.lxr.video_player.constants.MessageEvent 8 | import org.greenrobot.eventbus.EventBus 9 | 10 | /** 11 | * @Author : Liu XiaoRan 12 | * @Email : 592923276@qq.com 13 | * @Date : on 2023/1/29 11:45. 14 | * @Description : 15 | */ 16 | class BatteryReceiver : BroadcastReceiver() { 17 | 18 | var currentBattery: Int = -1 19 | override fun onReceive(context: Context?, intent: Intent?) { 20 | if (Intent.ACTION_BATTERY_CHANGED == intent!!.action) { 21 | val level = intent.getIntExtra("level", 0) 22 | if (currentBattery != level) { // 电量变化 23 | currentBattery = level 24 | EventBus.getDefault().post(MessageEvent(Constants.MSG_TYPE_BATTERY, currentBattery.toString())) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/ui/LocalVideoListActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.ui 2 | 3 | import android.content.Intent 4 | import android.os.Handler 5 | import android.view.View 6 | import android.widget.ProgressBar 7 | import android.widget.TextView 8 | import com.blankj.utilcode.util.* 9 | import com.bumptech.glide.Glide 10 | import com.bumptech.glide.load.engine.DiskCacheStrategy 11 | import com.drake.brv.annotaion.AnimationType 12 | import com.drake.brv.utils.bindingAdapter 13 | import com.drake.brv.utils.divider 14 | import com.drake.brv.utils.linear 15 | import com.drake.brv.utils.models 16 | import com.drake.brv.utils.setup 17 | import com.dyne.myktdemo.base.BaseActivity 18 | import com.hjq.bar.TitleBar 19 | import com.lxj.xpopup.XPopup 20 | import com.lxj.xpopup.interfaces.OnCancelListener 21 | import com.lxj.xpopup.interfaces.OnConfirmListener 22 | import com.lxr.video_player.R 23 | import com.lxr.video_player.constants.SimpleMessage 24 | import com.lxr.video_player.databinding.ActivityVideoListBinding 25 | import com.lxr.video_player.entity.VideoInfo 26 | import com.lxr.video_player.utils.SpUtil 27 | import com.lxr.video_player.utils.Utils 28 | import com.shuyu.gsyvideoplayer.utils.CommonUtil 29 | import org.greenrobot.eventbus.EventBus 30 | 31 | /** 32 | * @Author : Liu XiaoRan 33 | * @Email : 592923276@qq.com 34 | * @Date : on 2023/1/9 16:25. 35 | * @Description : 36 | */ 37 | class LocalVideoListActivity : BaseActivity() { 38 | 39 | /** 40 | * 当前文件夹id,由文件夹列表传进 41 | */ 42 | var bucketDisplayName: String? = "" 43 | 44 | override fun initView() { 45 | binding.titleBar.leftTitle = intent.getStringExtra("title") 46 | bucketDisplayName = intent.getStringExtra("bucketDisplayName") 47 | binding.rv.run { 48 | linear().divider(R.drawable.divider).setup { 49 | setAnimation(AnimationType.SLIDE_BOTTOM) 50 | addType(R.layout.item_video) 51 | onBind { 52 | Glide.with(context).load(getModel().path) 53 | .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) 54 | .placeholder(R.drawable.iv_video) 55 | .centerCrop().into(findView(R.id.iv)) 56 | // 总时长 57 | var duration = "" 58 | if (getModel().duration.toInt() == 0) { // 缺失时长/缩略图的,从缓存取(如果有) 59 | val cacheDuration = 60 | SPUtils.getInstance().getLong(getModel().id.toString(), 0L) 61 | if (cacheDuration != 0L) { // 有缓存 62 | duration = CommonUtil.stringForTime(cacheDuration) 63 | findView(R.id.progressBar).max = cacheDuration.toInt() 64 | } 65 | } else { // 正常视频 66 | duration = CommonUtil.stringForTime(getModel().duration) 67 | findView(R.id.progressBar).max = 68 | getModel().duration.toInt() 69 | } 70 | 71 | // 已播放进度时长 72 | val progressPlayed = SpUtil.getLong(getModel().id.toString()) 73 | if (progressPlayed != -0L && duration.isNotEmpty()) { // 有进度且有时长都显示 74 | findView(R.id.tv_duration).text = 75 | CommonUtil.stringForTime(progressPlayed!!) + "/" + duration 76 | findView(R.id.progressBar).progress = progressPlayed.toInt() 77 | } else { // 没有进度(也包括没时长,此时是空串,不耽误) 78 | findView(R.id.tv_duration).text = duration 79 | } 80 | } 81 | 82 | // 监听编辑(切换)模式,当前页面的backPress方法也实现了关闭方法,也会触发 83 | onToggle { position, toggleMode, end -> 84 | // 刷新列表,item的选择按钮根据开关显隐,所以要刷新 85 | val model = getModel(position) 86 | model.checkBoxVisibility = toggleMode 87 | // 数据变化.通知ui变化 88 | model.notifyChange() 89 | 90 | if (end) { // 列表遍历结束 91 | // 编辑菜单根据编辑模式的开关显隐 92 | binding.llMenu.visibility = if (toggleMode) View.VISIBLE else View.GONE 93 | // 如果取消编辑模式则取消全选,目前按返回键和和标题栏关闭多选时触发 94 | if (!toggleMode) checkedAll(false) 95 | } 96 | } 97 | 98 | onClick(R.id.item) { 99 | if (!toggleMode) { 100 | val intent = Intent(this@LocalVideoListActivity, PlayerActivity::class.java) 101 | intent.putExtra("videoList", GsonUtils.toJson(models)) 102 | intent.putExtra("position", modelPosition) 103 | startActivity(intent) 104 | } else { // 编辑模式,设置选择状态 105 | setChecked(layoutPosition, !getModel().checked) 106 | } 107 | } 108 | 109 | // 监听列表选中 110 | onChecked { position, isChecked, isAllChecked -> // 刷新当前选中条目状态 111 | val model = getModel(position) 112 | model.checked = isChecked 113 | model.notifyChange() 114 | 115 | // 编辑模式中的删除根据是否有选中条目是否启用 116 | if (bindingAdapter.checkedCount != 0) { 117 | binding.tvDelete.isEnabled = true 118 | binding.tvDelete.setTextColor(ColorUtils.getColor(R.color.red)) 119 | } else { 120 | binding.tvDelete.isEnabled = false 121 | binding.tvDelete.setTextColor(ColorUtils.getColor(R.color.disable_text)) 122 | } 123 | } 124 | 125 | // 长按 126 | onLongClick(R.id.item) { 127 | if (!toggleMode) { // 开启编辑模式 128 | toggle() 129 | // 并选中当前条 130 | setChecked(layoutPosition, true) 131 | } 132 | } 133 | } 134 | } 135 | 136 | initEditMode() 137 | } 138 | 139 | override fun onResume() { 140 | super.onResume() 141 | // 因为在这更新了数据,所以如果resume时候是编辑模式,更新后数据新的item的checkBoxVisibility默认是false,也就是隐藏状态,会导致开启了编辑(目前仅多选)模式,但是条目的checkBox是隐藏的 142 | // 所以先关闭编辑模式再获取数据(下策,且不该在resume随意刷新数据) 143 | binding.rv.bindingAdapter.toggle(false) 144 | updateListData() 145 | } 146 | 147 | override fun onRightClick(titleBar: TitleBar?) { 148 | binding.rv.bindingAdapter.toggle() // 点击事件触发切换事件 149 | } 150 | 151 | override fun onBackPressed() { 152 | if (binding.rv.bindingAdapter.toggleMode) { // 当前是编辑选择模式则关闭 153 | binding.rv.bindingAdapter.toggle(false) 154 | } else { 155 | super.onBackPressed() 156 | } 157 | } 158 | 159 | /** 160 | * 初始化编辑模式视图 161 | */ 162 | private fun initEditMode() { 163 | val adapter = binding.rv.bindingAdapter 164 | 165 | // 全选 166 | binding.tvAllChecked.setOnClickListener { 167 | adapter.checkedAll() 168 | } 169 | 170 | // 取消选择 171 | binding.tvCancelChecked.setOnClickListener { 172 | adapter.checkedAll(false) 173 | } 174 | 175 | // 删除 176 | binding.tvDelete.setOnClickListener { 177 | XPopup.Builder(this) 178 | .asConfirm( 179 | "已选择( ${adapter.checkedCount} / ${adapter.modelCount} )", 180 | "确定删除?", 181 | "点错了", 182 | "确定", 183 | object : OnConfirmListener { 184 | override fun onConfirm() { 185 | showLoading() 186 | adapter.getCheckedModels().forEach { 187 | if (FileUtils.delete(it.path)) { 188 | // 删除缓存的影片时长(部分系统获取不到时长的影片,已在播放的时候缓存) 189 | SPUtils.getInstance() 190 | .remove(it.id.toString()) 191 | // 删除缓存的播放进度 192 | SpUtil.removeKey(it.id.toString()) 193 | // 文件增删需要通知系统扫描,否则删除文件后还能查出来 194 | // 这个工具类直接传文件路径不知道为啥通知失败,手动获取一下 195 | FileUtils.notifySystemToScan( 196 | FileUtils.getDirName( 197 | it.path 198 | ) 199 | ) 200 | } else { 201 | ToastUtils.showShort("部分文件删除失败") 202 | } 203 | } 204 | EventBus.getDefault().post(SimpleMessage.REFRESH) 205 | adapter.toggle(false) 206 | } 207 | }, 208 | object : OnCancelListener { 209 | override fun onCancel() { 210 | adapter.toggle(false) 211 | } 212 | }, 213 | false 214 | ).show() 215 | } 216 | } 217 | 218 | /** 219 | * 更新电影列表,和文件夹列表一样全都遍历出来后再筛选出当前文件夹的媒体 todo 后续看看contentResolver 能否按指定文件夹把文件遍历出来 220 | */ 221 | private fun updateListData() { 222 | binding.rv.models = Utils.getVideoList().filter { // 筛选出当前文件夹的视频 223 | it.bucketDisplayName.equals(bucketDisplayName) 224 | }.toMutableList() 225 | } 226 | 227 | override fun onSimpleMessage(simpleMessage: String?) { 228 | if (simpleMessage.equals(SimpleMessage.REFRESH)) { 229 | Handler().postDelayed({ // 文件变动需要主动让系统扫描,为避免未扫描完毕,加个延迟 230 | updateListData() 231 | dismissLoading() 232 | }, 1000) 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.ui 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.viewpager2.adapter.FragmentStateAdapter 5 | import com.blankj.utilcode.util.ActivityUtils 6 | import com.blankj.utilcode.util.ToastUtils 7 | import com.dyne.myktdemo.base.BaseActivity 8 | import com.lxr.video_player.databinding.ActivityMainBinding 9 | 10 | class MainActivity : BaseActivity() { 11 | 12 | val fragments = listOf( 13 | MovieFoldersListFragment(), 14 | SettingFragment() 15 | ) 16 | 17 | override fun initView() { 18 | binding.vp2.adapter = object : FragmentStateAdapter(this@MainActivity) { 19 | override fun getItemCount(): Int = fragments.size 20 | 21 | override fun createFragment(position: Int): Fragment = fragments[position] 22 | } 23 | 24 | binding.vp2.isUserInputEnabled = false 25 | binding.bottomNav.setOnNavigationItemSelectedListener { 26 | binding.vp2.currentItem = it.order 27 | true 28 | } 29 | } 30 | 31 | var exitTime = 0L 32 | override fun onBackPressed() { 33 | // 是主页 34 | if (System.currentTimeMillis() - exitTime > 2000) { 35 | ToastUtils.showShort("再按一次退出程序") 36 | exitTime = System.currentTimeMillis() 37 | } else { 38 | ActivityUtils.finishAllActivities(true) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/ui/MovieFoldersListFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.ui 2 | 3 | import android.content.Intent 4 | import android.widget.ProgressBar 5 | import android.widget.TextView 6 | import androidx.core.view.isVisible 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import com.blankj.utilcode.util.ActivityUtils 9 | import com.blankj.utilcode.util.GsonUtils 10 | import com.blankj.utilcode.util.SPUtils 11 | import com.blankj.utilcode.util.ToastUtils 12 | import com.bumptech.glide.Glide 13 | import com.bumptech.glide.load.engine.DiskCacheStrategy 14 | import com.drake.brv.annotaion.AnimationType 15 | import com.drake.brv.utils.divider 16 | import com.drake.brv.utils.linear 17 | import com.drake.brv.utils.models 18 | import com.drake.brv.utils.setup 19 | import com.dyne.myktdemo.base.BaseFragment 20 | import com.hjq.permissions.OnPermissionCallback 21 | import com.hjq.permissions.Permission 22 | import com.hjq.permissions.XXPermissions 23 | import com.lxj.xpopup.XPopup 24 | import com.lxj.xpopup.interfaces.OnCancelListener 25 | import com.lxj.xpopup.interfaces.OnConfirmListener 26 | import com.lxr.video_player.R 27 | import com.lxr.video_player.constants.SimpleMessage 28 | import com.lxr.video_player.databinding.FragmentMovieFolderListBinding 29 | import com.lxr.video_player.entity.VideoFolder 30 | import com.lxr.video_player.entity.VideoInfo 31 | import com.lxr.video_player.utils.SpUtil 32 | import com.lxr.video_player.utils.Utils 33 | import com.shuyu.gsyvideoplayer.utils.CommonUtil 34 | 35 | /** 36 | * @Author : Liu XiaoRan 37 | * @Email : 592923276@qq.com 38 | * @Date : on 2023/1/11 13:52. 39 | * @Description : 40 | */ 41 | class MovieFoldersListFragment : BaseFragment() { 42 | /** 43 | * 文件夹集合 44 | */ 45 | private val folderList = mutableListOf() 46 | 47 | override fun initView() { 48 | binding.rvPlayHistory.run { // 初始化历史记录列表 49 | linear(orientation = LinearLayoutManager.HORIZONTAL).setup { 50 | addType(R.layout.item_play_history) 51 | onBind { 52 | Glide.with(context).load(getModel().path) 53 | .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) 54 | .placeholder(R.drawable.iv_video) 55 | .centerCrop().into(findView(R.id.iv)) 56 | // 总时长 57 | var duration = "" 58 | if (getModel().duration.toInt() == 0) { // 缺失时长/缩略图的,从缓存取(如果有) 59 | val cacheDuration = 60 | SPUtils.getInstance().getLong(getModel().id.toString(), 0L) 61 | if (cacheDuration != 0L) { // 有缓存 62 | duration = CommonUtil.stringForTime(cacheDuration) 63 | findView(R.id.progressBar).max = cacheDuration.toInt() 64 | } 65 | } else { // 正常视频 66 | duration = CommonUtil.stringForTime(getModel().duration) 67 | findView(R.id.progressBar).max = getModel().duration.toInt() 68 | } 69 | // 已播放进度时长 70 | val progressPlayed = SpUtil.getLong(getModel().id.toString()) 71 | if (progressPlayed != -0L && duration.isNotEmpty()) { // 有进度且有时长都显示 72 | findView(R.id.tv_duration).text = CommonUtil.stringForTime(progressPlayed!!) + "/" + duration 73 | findView(R.id.progressBar).progress = progressPlayed.toInt() 74 | } else { // 没有进度(也包括没时长,此时是空串,不耽误) 75 | findView(R.id.tv_duration).text = duration 76 | } 77 | } 78 | onClick(R.id.item) { 79 | val intent = Intent(this@MovieFoldersListFragment.context, PlayerActivity::class.java) 80 | intent.putExtra("videoList", GsonUtils.toJson(models)) 81 | intent.putExtra("position", modelPosition) 82 | startActivity(intent) 83 | } 84 | } 85 | } 86 | binding.rv.run { // 初始化文件夹列表 87 | linear().divider(R.drawable.divider).setup { 88 | setAnimation(AnimationType.SLIDE_BOTTOM) 89 | addType(R.layout.item_folder) 90 | onBind { 91 | Glide.with(context) 92 | .load(getModel().videoList[0].path) // 第一个视频做封面 93 | .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) 94 | .placeholder(R.drawable.iv_video) 95 | .centerCrop().into(findView(R.id.iv)) 96 | } 97 | onClick(R.id.item) { 98 | val intent = Intent( 99 | this@MovieFoldersListFragment.context, 100 | LocalVideoListActivity::class.java 101 | ) 102 | intent.putExtra("title", getModel().name) 103 | intent.putExtra("bucketDisplayName", getModel().videoList[0].bucketDisplayName) 104 | startActivity(intent) 105 | } 106 | } 107 | } 108 | showPermissionTipPopup() 109 | } 110 | 111 | override fun onResume() { 112 | super.onResume() 113 | this@MovieFoldersListFragment.context?.let { 114 | if (XXPermissions.isGranted(it, Permission.MANAGE_EXTERNAL_STORAGE)) { 115 | updateListData() 116 | } 117 | } 118 | } 119 | 120 | private fun showPermissionTipPopup() { 121 | this@MovieFoldersListFragment.context?.let { 122 | if (!XXPermissions.isGranted(it, Permission.MANAGE_EXTERNAL_STORAGE)) { 123 | XPopup.Builder(context) 124 | .dismissOnBackPressed(false) 125 | .dismissOnTouchOutside(false) 126 | .asConfirm( 127 | "提示", 128 | "为了播放视频、音频、获取字幕,我们需要访问您设备文件的权限", 129 | "就不给", 130 | "好哒", 131 | object : OnConfirmListener { 132 | override fun onConfirm() { 133 | getPermission2getData() 134 | } 135 | }, 136 | object : OnCancelListener { 137 | override fun onCancel() { 138 | ActivityUtils.finishAllActivities(true) 139 | } 140 | }, 141 | false 142 | ) 143 | .show() 144 | } else { 145 | updateListData() 146 | } 147 | } 148 | } 149 | 150 | private fun getPermission2getData() { 151 | XXPermissions.with(this) 152 | .permission(Permission.MANAGE_EXTERNAL_STORAGE) 153 | .request(object : OnPermissionCallback { 154 | 155 | override fun onGranted(permissions: MutableList, allGranted: Boolean) { 156 | if (!allGranted) { 157 | ToastUtils.showLong("部分权限未正常授予,请授权") 158 | return 159 | } 160 | updateListData() 161 | } 162 | 163 | override fun onDenied(permissions: MutableList, doNotAskAgain: Boolean) { 164 | if (doNotAskAgain) { 165 | ToastUtils.showLong("读写文件权限被永久拒绝,请手动授权") 166 | // 如果是被永久拒绝就跳转到应用权限系统设置页面 167 | XXPermissions.startPermissionActivity( 168 | this@MovieFoldersListFragment, 169 | permissions 170 | ) 171 | } else { 172 | showPermissionTipPopup() 173 | ToastUtils.showShort("获取权限失败") 174 | } 175 | } 176 | }) 177 | } 178 | 179 | private fun updateListData() { 180 | folderList.clear() 181 | // 全部视频 182 | val videoList = Utils.getVideoList() 183 | val groupByBucketIdMap = videoList.groupBy { // 按文件夹分组 184 | it.bucketDisplayName 185 | } 186 | for ((k, v) in groupByBucketIdMap) { // 按文件夹名字区分,一样的名字装在一起,每个分组的k为文件夹名字,值为所有包含当前key的对象的集合,设置到文件夹对象并装文件夹集合 187 | folderList.add(VideoFolder(k, v as MutableList)) 188 | } 189 | // 给列表设置数据 190 | binding.rv.models = folderList 191 | 192 | // 有历史记录的 193 | val mutablePlayHistoryList = videoList.filter { // 过滤出有历史播放进度和总时长的 194 | SpUtil.getLong(it.id.toString()) != 0L && ( 195 | SPUtils.getInstance() 196 | .getLong(it.id.toString(), 0L) != 0L || it.duration != 0L 197 | ) 198 | }.toMutableList() 199 | if (mutablePlayHistoryList.size != 0) { 200 | binding.llPlayHistoryContainer.isVisible = true 201 | binding.rvPlayHistory.models = mutablePlayHistoryList 202 | } 203 | } 204 | 205 | override fun onEvent(simpleMessage: String?) { 206 | if (simpleMessage.equals(SimpleMessage.REFRESH)) { 207 | updateListData() 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/ui/PlayerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.ui 2 | 3 | import android.content.Intent 4 | import android.content.IntentFilter 5 | import android.content.res.Configuration 6 | import android.util.DisplayMetrics 7 | import android.view.View 8 | import androidx.core.view.isVisible 9 | import com.blankj.utilcode.util.GsonUtils 10 | import com.blankj.utilcode.util.VibrateUtils 11 | import com.dyne.myktdemo.base.BaseActivity 12 | import com.github.gzuliyujiang.filepicker.ExplorerConfig 13 | import com.github.gzuliyujiang.filepicker.FilePicker 14 | import com.github.gzuliyujiang.filepicker.annotation.ExplorerMode 15 | import com.github.gzuliyujiang.filepicker.contract.OnFilePickedListener 16 | import com.google.common.reflect.TypeToken 17 | import com.lxj.xpopup.XPopup 18 | import com.lxj.xpopup.interfaces.OnSelectListener 19 | import com.lxr.video_player.action.OnLongPressUpListener 20 | import com.lxr.video_player.constants.Constants 21 | import com.lxr.video_player.constants.MessageEvent 22 | import com.lxr.video_player.databinding.ActivityPlayerBinding 23 | import com.lxr.video_player.entity.VideoInfo 24 | import com.lxr.video_player.receiver.BatteryReceiver 25 | import com.lxr.video_player.utils.SpUtil 26 | import com.shuyu.gsyvideoplayer.GSYVideoManager 27 | import com.shuyu.gsyvideoplayer.utils.OrientationUtils 28 | import me.jessyan.autosize.internal.CancelAdapt 29 | import java.io.File 30 | 31 | open class PlayerActivity : BaseActivity(), CancelAdapt { 32 | 33 | var orientationUtils: OrientationUtils? = null 34 | var batteryReceiver = BatteryReceiver() 35 | 36 | override fun initBeforeInitView() { 37 | initFontScale() 38 | } 39 | 40 | override fun initView() { 41 | init() 42 | registerReceiver(batteryReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) 43 | } 44 | 45 | override fun onEvent(event: MessageEvent) { 46 | if (event.type == Constants.MSG_TYPE_BATTERY) { 47 | binding.videoPlayer.updateBattery(event.message.toInt()) 48 | } 49 | } 50 | 51 | /** 52 | * 更改字体大小 53 | */ 54 | private fun initFontScale() { 55 | val configuration: Configuration = resources.configuration 56 | configuration.fontScale = 1F 57 | // 0.85 小, 1 标准大小, 1.15 大,1.3 超大 ,1.45 特大 58 | val metrics = DisplayMetrics() 59 | windowManager.defaultDisplay.getMetrics(metrics) 60 | metrics.scaledDensity = configuration.fontScale * metrics.density 61 | resources.updateConfiguration(configuration, metrics) 62 | } 63 | 64 | private fun init() { 65 | val videoListJson = intent.getStringExtra("videoList") 66 | val position = intent.getIntExtra("position", 0) 67 | val videoList = GsonUtils.fromJson>( 68 | videoListJson, 69 | object : TypeToken>() {}.type 70 | ) 71 | 72 | binding.videoPlayer.setUp(videoList, position) 73 | // 增加title 74 | binding.videoPlayer.titleTextView.visibility = View.VISIBLE 75 | // 设置返回键 76 | binding.videoPlayer.backButton.visibility = View.VISIBLE 77 | // 设置旋转 78 | orientationUtils = OrientationUtils(this, binding.videoPlayer) 79 | // 设置全屏按键功能,这是使用的是选择屏幕,而不是全屏 80 | binding.videoPlayer.fullscreenButton 81 | .setOnClickListener { // ------- !!!如果不需要旋转屏幕,可以不调用!!!------- 82 | // 不需要屏幕旋转,还需要设置 setNeedOrientationUtils(false) 83 | orientationUtils!!.resolveByClick() 84 | } 85 | // 是否可以滑动调整 86 | binding.videoPlayer.setIsTouchWiget(true) 87 | binding.videoPlayer.seekRatio = 50F 88 | // 设置返回按键功能 89 | binding.videoPlayer.backButton.setOnClickListener { onBackPressed() } 90 | binding.videoPlayer.isReleaseWhenLossAudio = false 91 | 92 | binding.videoPlayer.setOnLongPressListener(object : OnLongPressUpListener { 93 | // 长按监听 94 | override fun onLongPressIsStart(start: Boolean) { 95 | if (binding.videoPlayer.isInPlayingState) { 96 | if (start) { 97 | VibrateUtils.vibrate(100) 98 | binding.llSpeed.isVisible = true 99 | binding.videoPlayer.setSpeedPlaying(2F, false) 100 | } else { 101 | binding.llSpeed.isVisible = false 102 | binding.videoPlayer.setSpeedPlaying(1F, false) 103 | } 104 | } 105 | } 106 | }) 107 | binding.videoPlayer.getAddSubtitleView().setOnClickListener { 108 | binding.videoPlayer.onVideoPause() 109 | XPopup.Builder(this) 110 | .asCenterList( 111 | null, arrayOf("添加本地字幕", "隐藏本地字幕"), 112 | object : OnSelectListener { 113 | override fun onSelect(position: Int, text: String?) { 114 | if (position == 0) { 115 | val config = ExplorerConfig(this@PlayerActivity) 116 | config.rootDir = File(SpUtil.getString(Constants.K_DEFAULT_PATH_4_FIND_SUBTITLE)!!) 117 | config.isLoadAsync = true 118 | config.explorerMode = ExplorerMode.FILE 119 | config.allowExtensions = Constants.SUPPORT_SUBTITLE_TYPE 120 | config.onFilePickedListener = 121 | OnFilePickedListener { 122 | binding.videoPlayer.setSubTitle(it.absolutePath) 123 | } 124 | val picker = FilePicker(this@PlayerActivity) 125 | picker.setExplorerConfig(config) 126 | picker.show() 127 | } else { 128 | binding.videoPlayer.setSubTitle("") 129 | } 130 | } 131 | } 132 | ).show() 133 | } 134 | 135 | binding.videoPlayer.startPlayLogic() 136 | } 137 | 138 | override fun onPause() { 139 | super.onPause() 140 | binding.videoPlayer.onVideoPause() 141 | } 142 | 143 | override fun onResume() { 144 | super.onResume() 145 | binding.videoPlayer.onVideoResume() 146 | } 147 | 148 | override fun onDestroy() { 149 | super.onDestroy() 150 | unregisterReceiver(batteryReceiver) 151 | 152 | GSYVideoManager.releaseAllVideos() 153 | if (orientationUtils != null) orientationUtils!!.releaseListener() 154 | } 155 | 156 | override fun onBackPressed() { 157 | binding.videoPlayer.setVideoAllCallBack(null) 158 | super.onBackPressed() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/ui/SettingFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.ui 2 | 3 | import com.blankj.utilcode.util.SPUtils 4 | import com.dyne.myktdemo.base.BaseFragment 5 | import com.github.gzuliyujiang.filepicker.ExplorerConfig 6 | import com.github.gzuliyujiang.filepicker.FilePicker 7 | import com.github.gzuliyujiang.filepicker.annotation.ExplorerMode 8 | import com.github.gzuliyujiang.filepicker.contract.OnFilePickedListener 9 | import com.lxj.xpopup.XPopup 10 | import com.lxj.xpopup.interfaces.OnConfirmListener 11 | import com.lxr.video_player.R 12 | import com.lxr.video_player.constants.Constants 13 | import com.lxr.video_player.constants.SimpleMessage 14 | import com.lxr.video_player.databinding.FragmentSettingBinding 15 | import com.lxr.video_player.utils.SpUtil 16 | import com.shuyu.gsyvideoplayer.GSYVideoManager 17 | import com.shuyu.gsyvideoplayer.player.IjkPlayerManager 18 | import com.shuyu.gsyvideoplayer.player.PlayerFactory 19 | import com.shuyu.gsyvideoplayer.player.SystemPlayerManager 20 | import org.greenrobot.eventbus.EventBus 21 | import tv.danmaku.ijk.media.exo2.Exo2PlayerManager 22 | import java.io.File 23 | 24 | /** 25 | * @Author : Liu XiaoRan 26 | * @Email : 592923276@qq.com 27 | * @Date : on 2023/1/11 13:52. 28 | * @Description : 29 | */ 30 | class SettingFragment : BaseFragment() { 31 | 32 | override fun initView() { 33 | binding.tvSubtitlePath.text = SpUtil.getString(Constants.K_DEFAULT_PATH_4_FIND_SUBTITLE) 34 | 35 | binding.rlPlayerManager.setOnClickListener { 36 | XPopup.Builder(this@SettingFragment.context) 37 | .asCenterList( 38 | null, 39 | arrayOf( 40 | resources.getString(R.string.player_manager_ijk), 41 | resources.getString(R.string.player_manager_exo), 42 | resources.getString(R.string.player_manager_system) 43 | ) 44 | ) { position, _ -> 45 | when (position) { 46 | 0 -> { 47 | PlayerFactory.setPlayManager(IjkPlayerManager::class.java) 48 | binding.tvPlayerManager.text = resources.getString(R.string.player_manager_ijk) 49 | } 50 | 1 -> { 51 | PlayerFactory.setPlayManager(Exo2PlayerManager::class.java) 52 | binding.tvPlayerManager.text = resources.getString(R.string.player_manager_exo) 53 | } 54 | 2 -> { 55 | PlayerFactory.setPlayManager(SystemPlayerManager::class.java) 56 | binding.tvPlayerManager.text = resources.getString(R.string.player_manager_system) 57 | } 58 | } 59 | }.show() 60 | } 61 | 62 | binding.rlClearCache.setOnClickListener { 63 | XPopup.Builder(this@SettingFragment.context) 64 | .asConfirm( 65 | "提示", 66 | "确认清理缓存?", 67 | object : OnConfirmListener { 68 | override fun onConfirm() { 69 | SpUtil.clearAll() 70 | SPUtils.getInstance().clear() 71 | GSYVideoManager.instance().clearAllDefaultCache(this@SettingFragment.context) 72 | // 通知视频列表等,清楚通过缓存获取的状态(播放记录及播放时间等) 73 | EventBus.getDefault().post(SimpleMessage.REFRESH) 74 | } 75 | } 76 | ).show() 77 | } 78 | 79 | binding.rlSubtitlePath.setOnClickListener { 80 | val config = ExplorerConfig(context) 81 | config.rootDir = File(SpUtil.getString(Constants.K_DEFAULT_PATH_4_FIND_SUBTITLE)!!) 82 | config.explorerMode = ExplorerMode.DIRECTORY 83 | config.setOnFilePickedListener(object : OnFilePickedListener { 84 | override fun onFilePicked(file: File) { 85 | binding.tvSubtitlePath.text = file.absolutePath 86 | SpUtil.put(Constants.K_DEFAULT_PATH_4_FIND_SUBTITLE, file.absolutePath) 87 | } 88 | }) 89 | val picker = FilePicker(activity) 90 | picker.setExplorerConfig(config) 91 | picker.okView.text = "就用这个目录" 92 | picker.show() 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/ui/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.ui 2 | 3 | import android.content.Intent 4 | import android.os.Handler 5 | import com.dyne.myktdemo.base.BaseActivity 6 | import com.lxr.video_player.databinding.ActivitySplashBinding 7 | 8 | class SplashActivity : BaseActivity() { 9 | 10 | override fun initView() { 11 | Handler().postDelayed({ 12 | startActivity(Intent(this@SplashActivity, MainActivity::class.java)) 13 | finish() 14 | },500) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/utils/SpUtil.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.utils 2 | 3 | import android.os.Parcelable 4 | import com.tencent.mmkv.MMKV 5 | import java.util.* 6 | 7 | /** 8 | * @Author : Liu XiaoRan 9 | * @Email : 592923276@qq.com 10 | * @Date : on 2022/10/25 17:38. 11 | * @Description : 12 | */ 13 | object SpUtil { 14 | var mmkv: MMKV? = null 15 | 16 | init { 17 | mmkv = MMKV.defaultMMKV() 18 | } 19 | 20 | fun put(key: String, value: Any?) { 21 | when (value) { 22 | is String -> mmkv?.encode(key, value) 23 | is Float -> mmkv?.encode(key, value) 24 | is Boolean -> mmkv?.encode(key, value) 25 | is Int -> mmkv?.encode(key, value) 26 | is Long -> mmkv?.encode(key, value) 27 | is Double -> mmkv?.encode(key, value) 28 | is ByteArray -> mmkv?.encode(key, value) 29 | is Nothing -> return 30 | } 31 | } 32 | 33 | fun put(key: String, t: T?) { 34 | if (t == null) { 35 | return 36 | } 37 | mmkv?.encode(key, t) 38 | } 39 | 40 | fun put(key: String, sets: Set?) { 41 | if (sets == null) { 42 | return 43 | } 44 | mmkv?.encode(key, sets) 45 | } 46 | 47 | fun getInt(key: String): Int? { 48 | return mmkv?.decodeInt(key, 0) 49 | } 50 | 51 | fun getDouble(key: String): Double? { 52 | return mmkv?.decodeDouble(key, 0.00) 53 | } 54 | 55 | fun getLong(key: String): Long? { 56 | return mmkv?.decodeLong(key, 0L) 57 | } 58 | 59 | fun getBoolean(key: String): Boolean? { 60 | return mmkv?.decodeBool(key, false) 61 | } 62 | 63 | fun getFloat(key: String): Float? { 64 | return mmkv?.decodeFloat(key, 0F) 65 | } 66 | 67 | fun getByteArray(key: String): ByteArray? { 68 | return mmkv?.decodeBytes(key) 69 | } 70 | 71 | fun getString(key: String): String? { 72 | return mmkv?.decodeString(key, "") 73 | } 74 | 75 | fun getParcelable(key: String, tClass: Class): T? { 76 | return mmkv?.decodeParcelable(key, tClass) 77 | } 78 | 79 | fun getStringSet(key: String): Set? { 80 | return mmkv?.decodeStringSet(key, Collections.emptySet()) 81 | } 82 | 83 | fun removeKey(key: String) { 84 | mmkv?.removeValueForKey(key) 85 | } 86 | 87 | fun clearAll() { 88 | mmkv?.clearAll() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.utils 2 | 3 | import android.provider.MediaStore 4 | import com.lxr.video_player.MyApp 5 | import com.lxr.video_player.entity.VideoInfo 6 | 7 | /** 8 | * @Author : Liu XiaoRan 9 | * @Email : 592923276@qq.com 10 | * @Date : on 2023/1/9 09:39. 11 | * @Description : 12 | */ 13 | object Utils { 14 | 15 | fun getVideoList(): MutableList { 16 | val videoList: MutableList = mutableListOf() 17 | val cursor = MyApp.instance.contentResolver.query( 18 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 19 | arrayOf( // 查询内容 20 | MediaStore.Video.Media._ID, // 视频id 21 | MediaStore.Video.Media.DATA, // 视频路径 22 | MediaStore.Video.Media.SIZE, // 视频字节大小 23 | MediaStore.Video.Media.DISPLAY_NAME, // 视频名称 xxx.mp4 24 | MediaStore.Video.Media.TITLE, // 视频标题 25 | MediaStore.Video.Media.DURATION, // 视频时长 26 | MediaStore.Video.Media.RESOLUTION, // 视频分辨率 X x Y格式 27 | MediaStore.Video.Media.IS_PRIVATE, 28 | MediaStore.Video.Media.BUCKET_ID, 29 | MediaStore.Video.Media.BUCKET_DISPLAY_NAME, 30 | MediaStore.Video.Media.BOOKMARK // 上次视频播放的位置 31 | ), 32 | null, 33 | null, 34 | null 35 | ) 36 | if (cursor != null && cursor.moveToFirst()) { 37 | do { 38 | val videoInfo = VideoInfo() 39 | videoInfo.id = 40 | cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)) 41 | videoInfo.path = 42 | cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)) 43 | videoInfo.size = 44 | cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)) 45 | videoInfo.displayName = 46 | cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)) 47 | videoInfo.title = 48 | cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.TITLE)) 49 | videoInfo.duration = 50 | cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)) 51 | videoInfo.resolution = 52 | cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.RESOLUTION)) 53 | videoInfo.isPrivate = 54 | cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.IS_PRIVATE)) 55 | videoInfo.bucketId = 56 | cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_ID)) 57 | videoInfo.bucketDisplayName = 58 | cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_DISPLAY_NAME)) 59 | videoInfo.bookmark = 60 | cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.BOOKMARK)) 61 | videoList.add(videoInfo) 62 | } while (cursor.moveToNext()) 63 | cursor.close() 64 | } 65 | return videoList 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/widget/CustomPlayer.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.widget 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.media.AudioManager 7 | import android.text.TextUtils 8 | import android.util.AttributeSet 9 | import android.util.TypedValue 10 | import android.view.* 11 | import android.view.GestureDetector.SimpleOnGestureListener 12 | import android.widget.ImageView 13 | import com.blankj.utilcode.util.SPUtils 14 | import com.blankj.utilcode.util.ToastUtils 15 | import com.google.android.exoplayer2.Player 16 | import com.google.android.exoplayer2.text.CueGroup 17 | import com.google.android.exoplayer2.ui.CaptionStyleCompat 18 | import com.google.android.exoplayer2.ui.SubtitleView 19 | import com.lxr.video_player.R 20 | import com.lxr.video_player.action.OnLongPressUpListener 21 | import com.lxr.video_player.entity.VideoInfo 22 | import com.lxr.video_player.utils.SpUtil 23 | import com.lxr.video_player.widget.subtitle.GSYExoSubTitlePlayerManager 24 | import com.lxr.video_player.widget.subtitle.GSYExoSubTitleVideoManager 25 | import com.shuyu.gsyvideoplayer.listener.GSYStateUiListener 26 | import com.shuyu.gsyvideoplayer.utils.Debuger 27 | import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer 28 | import com.shuyu.gsyvideoplayer.video.base.GSYBaseVideoPlayer 29 | import com.shuyu.gsyvideoplayer.video.base.GSYVideoPlayer 30 | import com.shuyu.gsyvideoplayer.video.base.GSYVideoView 31 | 32 | /** 33 | * @Author : Liu XiaoRan 34 | * @Email : 592923276@qq.com 35 | * @Date : on 2023/1/12 14:31. 36 | * @Description : 自定义播放器,添加:倍速播放,下一集,缓存进度和一些丢失时长信息的视频 37 | */ 38 | class CustomPlayer : StandardGSYVideoPlayer, Player.Listener { 39 | 40 | lateinit var mSubtitleView: SubtitleView 41 | private var mSubTitle: String? = "" 42 | 43 | /** 44 | * 长按倍速标识,仅长按时开启,长按结束的MotionEvent.ACTION_UP时再关闭,避免点击时也触发倍速播放 45 | */ 46 | private var idLongPressSpeed = false 47 | 48 | /** 49 | * 长按/抬起监听器 50 | */ 51 | private var pressUpListener: OnLongPressUpListener? = null 52 | 53 | constructor(context: Context?, fullFlag: Boolean?) : super(context, fullFlag) 54 | constructor(context: Context?) : super(context) 55 | constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) 56 | 57 | private lateinit var batteryView: MyBatteryView 58 | private lateinit var ivAddSubtitle: ImageView 59 | 60 | private var uriList: List = ArrayList() 61 | 62 | override fun getLayoutId(): Int { 63 | return R.layout.widget_custom_video 64 | } 65 | 66 | override fun init(context: Context?) { 67 | super.init(context) 68 | mSubtitleView = findViewById(R.id.sub_title_view) 69 | mSubtitleView.setStyle( 70 | CaptionStyleCompat( 71 | Color.WHITE, 72 | Color.TRANSPARENT, 73 | Color.TRANSPARENT, 74 | CaptionStyleCompat.EDGE_TYPE_NONE, 75 | CaptionStyleCompat.EDGE_TYPE_NONE, 76 | null 77 | ) 78 | ) 79 | mSubtitleView.setFixedTextSize(TypedValue.COMPLEX_UNIT_DIP, 16f) 80 | batteryView = findViewById(R.id.battery) 81 | findViewById(R.id.iv_next).setOnClickListener { 82 | playNext() 83 | } 84 | ivAddSubtitle = findViewById(R.id.iv_subtitle) 85 | 86 | post { 87 | gestureDetector = GestureDetector( 88 | getContext().applicationContext, 89 | object : SimpleOnGestureListener() { 90 | override fun onDoubleTap(e: MotionEvent): Boolean { 91 | touchDoubleUp(e) 92 | return super.onDoubleTap(e) 93 | } 94 | 95 | override fun onSingleTapConfirmed(e: MotionEvent): Boolean { 96 | if (!mChangePosition && !mChangeVolume && !mBrightness) { 97 | onClickUiToggle(e) 98 | } 99 | return super.onSingleTapConfirmed(e) 100 | } 101 | 102 | override fun onLongPress(e: MotionEvent) { 103 | idLongPressSpeed = true 104 | pressUpListener?.onLongPressIsStart(true) 105 | } 106 | } 107 | ) 108 | } 109 | } 110 | 111 | /** 112 | * 获取添加字幕view 113 | */ 114 | fun getAddSubtitleView(): ImageView { 115 | return ivAddSubtitle 116 | } 117 | 118 | /** 119 | * 设置播放URL 120 | * 121 | * @param url 播放url,这里用集合,以供播放下一集等使用 122 | * @param cacheWithPlay 是否边播边缓存 123 | * @param position 需要播放的位置 124 | * @param cachePath 缓存路径,如果是M3U8或者HLS,请设置为false 125 | * @param mapHeadData http header 126 | * @param changeState 切换的时候释放surface 127 | * @return 128 | */ 129 | fun setUp( 130 | urls: List, 131 | position: Int, 132 | ): Boolean { 133 | uriList = urls 134 | mPlayPosition = position 135 | val videoModel = urls[position] 136 | val set = 137 | setUp(videoModel.path, false, videoModel.title) 138 | if (!TextUtils.isEmpty(videoModel.title) && mTitleTextView != null) { 139 | mTitleTextView.text = videoModel.title 140 | } 141 | 142 | // 设置当前视频id,缓存当前播放进度和时长 143 | val currentVideoId = videoModel.id.toString() 144 | // 监听播放状态通过视频id缓存播放进度和时长 145 | gsyStateUiListener = GSYStateUiListener { state -> 146 | when (state) { 147 | GSYVideoView.CURRENT_STATE_PAUSE, GSYVideoView.CURRENT_STATE_ERROR -> { 148 | currentVideoId.let { 149 | // 直接home退出/暂停/返回,播放下一集,存储当前影片的播放进度 150 | SpUtil.put(it, currentPositionWhenPlaying) 151 | // 部分资源没有时长(部分媒体文件用几种系统api都获取不到),在缓存中记录时长 注:这里用sp存时长,,而不是自己的(用id存播放进度了..后续可改用数据库记录每个电影的播放进度.总时长.缩略图.帧图等) 152 | if (videoModel.duration == 0L) { // 视频没有时长,自己缓存,用于下次显示 153 | SPUtils.getInstance().put(it, duration) 154 | } 155 | } 156 | } 157 | GSYVideoView.CURRENT_STATE_AUTO_COMPLETE -> currentVideoId.let { // 自动播放完成完成清空该影片缓存的进度 158 | SpUtil.removeKey(it) 159 | } 160 | } 161 | } 162 | 163 | // 设置播放进度 164 | val playPosition = SpUtil.getLong(currentVideoId) 165 | if (playPosition != null) { // 设置播放位置 166 | seekOnStart = playPosition 167 | } 168 | 169 | return set 170 | } 171 | 172 | /** 173 | * 设置长按/抬起监听器 174 | */ 175 | fun setOnLongPressListener(onLongPressUpListener: OnLongPressUpListener) { 176 | pressUpListener = onLongPressUpListener 177 | } 178 | 179 | override fun onTouch(v: View?, event: MotionEvent?): Boolean { 180 | if (idLongPressSpeed && event?.action == MotionEvent.ACTION_UP) { 181 | idLongPressSpeed = false 182 | pressUpListener?.onLongPressIsStart(false) 183 | } 184 | return super.onTouch(v, event) 185 | } 186 | 187 | /** 188 | * 播放下一集 189 | * 190 | * @return true表示还有下一集 191 | */ 192 | private fun playNext(): Boolean { 193 | if (mPlayPosition < uriList.size - 1) { 194 | // 直接home退出/暂停/返回,播放下一集,存储当前影片的播放进度 195 | SpUtil.put(uriList[mPlayPosition].id.toString(), currentPositionWhenPlaying) 196 | // 记录下一集播放位置 197 | mPlayPosition += 1 198 | val nextVideoModel: VideoInfo = uriList[mPlayPosition] 199 | mSaveChangeViewTIme = 0 200 | setUp(uriList, mPlayPosition) 201 | if (!TextUtils.isEmpty(nextVideoModel.title) && mTitleTextView != null) { 202 | mTitleTextView.text = nextVideoModel.title 203 | } 204 | startPlayLogic() 205 | return true 206 | } else { 207 | ToastUtils.showShort("最后一集了") 208 | } 209 | return false 210 | } 211 | 212 | fun updateBattery(battery: Int) { 213 | batteryView.updateBattery(battery) 214 | } 215 | 216 | override fun startPrepare() { 217 | gsyVideoManager?.listener()?.onCompletion() 218 | if (mVideoAllCallBack != null) { 219 | Debuger.printfLog("onStartPrepared") 220 | mVideoAllCallBack.onStartPrepared(mOriginUrl, mTitle, this) 221 | } 222 | gsyVideoManager?.setListener(this) 223 | gsyVideoManager?.setPlayTag(mPlayTag) 224 | gsyVideoManager?.setPlayPosition(mPlayPosition) 225 | mAudioManager.requestAudioFocus( 226 | onAudioFocusChangeListener, 227 | AudioManager.STREAM_MUSIC, 228 | AudioManager.AUDIOFOCUS_GAIN_TRANSIENT 229 | ) 230 | try { 231 | if (mContext is Activity) { 232 | (mContext as Activity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 233 | } 234 | } catch (e: Exception) { 235 | e.printStackTrace() 236 | } 237 | mBackUpPlayingBufferState = -1 238 | gsyVideoManager?.prepare( 239 | mUrl, 240 | mSubTitle, 241 | this, 242 | if (mMapHeadData == null) HashMap() else mMapHeadData, 243 | mLooping, 244 | mSpeed, 245 | mCache, 246 | mCachePath, 247 | mOverrideExtension 248 | ) 249 | setStateAndUi(CURRENT_STATE_PREPAREING) 250 | } 251 | 252 | override fun onCues(cueGroup: CueGroup) { 253 | mSubtitleView.setCues(cueGroup.cues) 254 | } 255 | 256 | fun setSubTitle(subTitle: String?) { 257 | mSubTitle = subTitle 258 | // 不知道有什么方法播放中加载字幕,所以设置后重新播放(需要加载字幕的大多数是在刚开始观看的时候,直接重播应该不影响使用体验,或者加载后记录播放进度,直接seekTo) 259 | startPlayLogic() 260 | } 261 | 262 | /**********以下重载 GSYVideoPlayer 的 全屏 SubtitleView 相关实现 */ 263 | override fun startWindowFullscreen( 264 | context: Context?, 265 | actionBar: Boolean, 266 | statusBar: Boolean 267 | ): GSYBaseVideoPlayer? { 268 | val gsyBaseVideoPlayer = super.startWindowFullscreen(context, actionBar, statusBar) 269 | val gsyExoSubTitleVideoView: CustomPlayer = 270 | gsyBaseVideoPlayer as CustomPlayer 271 | (GSYExoSubTitleVideoManager.instance().player as GSYExoSubTitlePlayerManager) 272 | .addTextOutputPlaying(gsyExoSubTitleVideoView) 273 | return gsyBaseVideoPlayer 274 | } 275 | 276 | override fun resolveNormalVideoShow( 277 | oldF: View?, 278 | vp: ViewGroup?, 279 | gsyVideoPlayer: GSYVideoPlayer? 280 | ) { 281 | super.resolveNormalVideoShow(oldF, vp, gsyVideoPlayer) 282 | val gsyExoSubTitleVideoView: CustomPlayer? = 283 | gsyVideoPlayer as CustomPlayer? 284 | (GSYExoSubTitleVideoManager.instance().player as GSYExoSubTitlePlayerManager) 285 | .removeTextOutput(gsyExoSubTitleVideoView) 286 | } 287 | 288 | override fun getGSYVideoManager(): GSYExoSubTitleVideoManager? { 289 | GSYExoSubTitleVideoManager.instance().initContext(context.applicationContext) 290 | return GSYExoSubTitleVideoManager.instance() 291 | } 292 | 293 | override fun backFromFull(context: Context?): Boolean { 294 | return GSYExoSubTitleVideoManager.backFromWindowFull(context) 295 | } 296 | 297 | override fun releaseVideos() { 298 | GSYExoSubTitleVideoManager.releaseAllVideos() 299 | } 300 | 301 | override fun getFullId(): Int { 302 | return GSYExoSubTitleVideoManager.FULLSCREEN_ID 303 | } 304 | 305 | override fun getSmallId(): Int { 306 | return GSYExoSubTitleVideoManager.SMALL_ID 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/widget/MyBatteryView.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.widget 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.graphics.RectF 8 | import android.util.AttributeSet 9 | import android.view.View 10 | import androidx.core.graphics.ColorUtils 11 | import com.lxr.video_player.R 12 | 13 | /** 14 | * @Author : Liu XiaoRan 15 | * @Email : 592923276@qq.com 16 | * @Date : on 2023/2/1 14:11. 17 | * @Description : 18 | */ 19 | class MyBatteryView : View { 20 | constructor(context: Context) : this(context, null) 21 | constructor( 22 | context: Context, 23 | attrs: AttributeSet? = null 24 | ) : super(context, attrs) 25 | 26 | private var battery: Int = 80 27 | 28 | private val OUTLINE_STROKE_SIZE = 3.0f // 外层电池框厚度 29 | private val CORNER = 4f // 通用圆角 30 | 31 | private val CAP_WIDTH = 2.0f // 电池盖宽度 32 | private val CAP_HEIGHT = 10.0f // 电池盖高度 33 | 34 | private val BATTERY_MARGIN = 4f // 电量到电池框的距离 35 | 36 | // 外边框 37 | private val boxOut by lazy { 38 | RectF().also { 39 | // 从指定坐标开始画的话 坐标的位置貌似是在stroke线内 所以左和上坐标是 线宽 OUTLINE_STROKE_SIZE 40 | it.left = OUTLINE_STROKE_SIZE 41 | it.top = OUTLINE_STROKE_SIZE 42 | } 43 | } 44 | 45 | // 正极 46 | private val boxCap = RectF() 47 | 48 | // 电量 49 | private val boxBattery = RectF() 50 | 51 | // 外边框画笔 52 | private val boxOutPaint by lazy { 53 | Paint().also { 54 | it.color = Color.WHITE 55 | it.style = Paint.Style.STROKE 56 | it.strokeWidth = OUTLINE_STROKE_SIZE 57 | it.isAntiAlias = true 58 | } 59 | } 60 | 61 | // 正极画笔 62 | private val boxCapPaint by lazy { 63 | Paint().also { 64 | it.color = Color.WHITE 65 | it.style = Paint.Style.FILL 66 | it.isAntiAlias = true 67 | } 68 | } 69 | 70 | // 电量画笔 71 | private val boxBatteryPaint by lazy { 72 | Paint().also { 73 | it.color = Color.GREEN 74 | it.style = Paint.Style.FILL 75 | it.isAntiAlias = true 76 | } 77 | } 78 | 79 | // 电量文字画笔 80 | private val boxBatteryTextPaint by lazy { 81 | Paint().also { 82 | it.color = Color.WHITE 83 | it.style = Paint.Style.FILL_AND_STROKE 84 | it.textSize = 20f 85 | it.textAlign = Paint.Align.CENTER 86 | it.strokeWidth = 2f 87 | } 88 | } 89 | 90 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 91 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 92 | val specWidthSize = MeasureSpec.getSize(widthMeasureSpec) 93 | val specHeightSize = MeasureSpec.getSize(heightMeasureSpec) 94 | // 外边框的右侧应该在总长度 - 设定的盖的宽度 - 画笔的线的宽度 95 | boxOut.right = specWidthSize - CAP_WIDTH - OUTLINE_STROKE_SIZE 96 | boxOut.bottom = specHeightSize - OUTLINE_STROKE_SIZE 97 | 98 | // 电量框测量位置 99 | boxBattery.left = boxOut.left + BATTERY_MARGIN 100 | boxBattery.top = boxOut.top + BATTERY_MARGIN 101 | boxBattery.bottom = boxOut.bottom - BATTERY_MARGIN 102 | 103 | // 正极测量位置 104 | boxCap.left = boxOut.right 105 | boxCap.top = specHeightSize / 2 - CAP_HEIGHT / 2 106 | boxCap.right = specWidthSize.toFloat() 107 | boxCap.bottom = specHeightSize / 2 + CAP_HEIGHT / 2 108 | 109 | setMeasuredDimension(specWidthSize, specHeightSize) 110 | } 111 | 112 | override fun onDraw(canvas: Canvas?) { 113 | super.onDraw(canvas) 114 | 115 | // 满电的长度 116 | val fullPowerSize = boxOut.right - BATTERY_MARGIN - boxBattery.left 117 | 118 | boxBattery.right = boxBattery.left + fullPowerSize * (battery.toFloat() / 100) 119 | if (battery <= 20) { 120 | boxBatteryPaint.color = com.blankj.utilcode.util.ColorUtils.getColor(R.color.warn) 121 | } 122 | canvas?.drawRoundRect(boxOut, CORNER, CORNER, boxOutPaint) 123 | canvas?.drawRoundRect(boxCap, 1F, 1F, boxCapPaint) 124 | canvas?.drawRoundRect(boxBattery, CORNER / 2, CORNER / 2, boxBatteryPaint) 125 | 126 | val fontMetrics : Paint.FontMetrics = boxBatteryTextPaint.fontMetrics; 127 | // 计算文字高度 128 | val fontHeight = fontMetrics.bottom - fontMetrics.top 129 | // 计算文字baseline 130 | val textBaseY = height - (height - fontHeight) / 2 - fontMetrics.bottom; 131 | canvas?.drawText(battery.toString(), boxOut.centerX(), textBaseY, boxBatteryTextPaint) 132 | } 133 | 134 | fun updateBattery(battery: Int) { 135 | // this.battery = battery > 100 ? 100 : battery < 1 ? 1 : battery; 136 | this.battery = battery 137 | invalidate() 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/widget/SpaceItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.widget 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.Canvas 5 | import android.graphics.Rect 6 | import android.view.View 7 | 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import androidx.recyclerview.widget.RecyclerView 10 | 11 | 12 | class SpaceItemDecoration(private val leftRight: Int, private val topBottom: Int,private val firstNeedTop:Boolean = true) : RecyclerView.ItemDecoration() { 13 | //leftRight为横向间的距离 topBottom为纵向间距离 14 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 15 | super.onDraw(c, parent, state) 16 | } 17 | 18 | @SuppressLint("WrongConstant") 19 | override fun getItemOffsets( 20 | outRect: Rect, 21 | view: View, 22 | parent: RecyclerView, 23 | state: RecyclerView.State 24 | ) { 25 | val layoutManager = parent.layoutManager as LinearLayoutManager? 26 | //竖直方向的 27 | if (layoutManager!!.orientation == LinearLayoutManager.VERTICAL) { 28 | //最后一项需要 bottom 29 | if (parent.getChildAdapterPosition(view) == layoutManager.itemCount - 1) { 30 | outRect.bottom = topBottom 31 | } 32 | if(!firstNeedTop&&parent.getChildAdapterPosition(view)==0){ 33 | outRect.top = 0 34 | }else{ 35 | outRect.top = topBottom 36 | } 37 | outRect.left = leftRight 38 | outRect.right = leftRight 39 | } else { 40 | //最后一项需要right 41 | if (parent.getChildAdapterPosition(view) != layoutManager.itemCount - 1) { 42 | outRect.right = leftRight 43 | } 44 | outRect.top = topBottom 45 | outRect.left = 0 46 | outRect.bottom = topBottom 47 | } 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/widget/subtitle/GSYExoSubTitleModel.java: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.widget.subtitle; 2 | 3 | import com.google.android.exoplayer2.Player; 4 | import com.shuyu.gsyvideoplayer.model.GSYModel; 5 | 6 | import java.io.File; 7 | import java.util.Map; 8 | 9 | /** 10 | * Created by guoshuyu on 2018/5/16. 11 | * 自定义列表数据model 12 | */ 13 | 14 | public class GSYExoSubTitleModel extends GSYModel { 15 | 16 | private String subTitle; 17 | private Player.Listener textOutput; 18 | 19 | public GSYExoSubTitleModel(String url, String subTitle, Player.Listener textOutput, Map mapHeadData, boolean loop, float speed, boolean cache, File cachePath, String overrideExtension) { 20 | super(url, mapHeadData, loop, speed, cache, cachePath, overrideExtension); 21 | this.subTitle = subTitle; 22 | this.textOutput = textOutput; 23 | } 24 | 25 | public String getSubTitle() { 26 | return subTitle; 27 | } 28 | 29 | public void setSubTitle(String subTitle) { 30 | this.subTitle = subTitle; 31 | } 32 | 33 | public Player.Listener getTextOutput() { 34 | return textOutput; 35 | } 36 | 37 | public void setTextOutput(Player.Listener textOutput) { 38 | this.textOutput = textOutput; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/widget/subtitle/GSYExoSubTitlePlayer.java: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.widget.subtitle; 2 | 3 | import static com.google.android.exoplayer2.util.Assertions.checkNotNull; 4 | 5 | import android.content.Context; 6 | import android.net.Uri; 7 | import android.os.Handler; 8 | import android.os.Looper; 9 | 10 | import com.google.android.exoplayer2.C; 11 | import com.google.android.exoplayer2.DefaultLoadControl; 12 | import com.google.android.exoplayer2.DefaultRenderersFactory; 13 | import com.google.android.exoplayer2.ExoPlayer; 14 | import com.google.android.exoplayer2.Format; 15 | import com.google.android.exoplayer2.MediaItem; 16 | import com.google.android.exoplayer2.Player; 17 | import com.google.android.exoplayer2.metadata.Metadata; 18 | import com.google.android.exoplayer2.source.MediaSource; 19 | import com.google.android.exoplayer2.source.MergingMediaSource; 20 | import com.google.android.exoplayer2.source.SingleSampleMediaSource; 21 | import com.google.android.exoplayer2.text.Cue; 22 | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; 23 | import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; 24 | import com.google.android.exoplayer2.upstream.DefaultDataSource; 25 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; 26 | import com.google.android.exoplayer2.util.MimeTypes; 27 | 28 | import java.util.List; 29 | 30 | import tv.danmaku.ijk.media.exo2.IjkExo2MediaPlayer; 31 | import tv.danmaku.ijk.media.exo2.demo.EventLogger; 32 | 33 | public class GSYExoSubTitlePlayer extends IjkExo2MediaPlayer { 34 | 35 | private String mSubTitile; 36 | private Player.Listener mTextOutput; 37 | 38 | public GSYExoSubTitlePlayer(Context context) { 39 | super(context); 40 | } 41 | 42 | 43 | @Override 44 | public void onCues(List cues) { 45 | super.onCues(cues); 46 | /// 这里 47 | } 48 | 49 | @Override 50 | public void onMetadata(Metadata metadata) { 51 | super.onMetadata(metadata); 52 | /// 这里 53 | } 54 | 55 | @Override 56 | protected void prepareAsyncInternal() { 57 | new Handler(Looper.getMainLooper()).post( 58 | new Runnable() { 59 | @Override 60 | public void run() { 61 | if (mTrackSelector == null) { 62 | mTrackSelector = new DefaultTrackSelector(mAppContext); 63 | } 64 | mEventLogger = new EventLogger(mTrackSelector); 65 | boolean preferExtensionDecoders = true; 66 | boolean useExtensionRenderers = true;//是否开启扩展 67 | @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = useExtensionRenderers 68 | ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER 69 | : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) 70 | : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; 71 | if (mRendererFactory == null) { 72 | mRendererFactory = new DefaultRenderersFactory(mAppContext); 73 | mRendererFactory.setExtensionRendererMode(extensionRendererMode); 74 | } 75 | if (mLoadControl == null) { 76 | mLoadControl = new DefaultLoadControl(); 77 | } 78 | mInternalPlayer = new ExoPlayer.Builder(mAppContext, mRendererFactory) 79 | .setLooper(Looper.getMainLooper()) 80 | .setTrackSelector(mTrackSelector) 81 | .setLoadControl(mLoadControl).build(); 82 | mInternalPlayer.addListener(GSYExoSubTitlePlayer.this); 83 | mInternalPlayer.addAnalyticsListener(GSYExoSubTitlePlayer.this); 84 | if (mTextOutput != null) { 85 | mInternalPlayer.addListener(mTextOutput); 86 | } 87 | mInternalPlayer.addListener(mEventLogger); 88 | if (mSpeedPlaybackParameters != null) { 89 | mInternalPlayer.setPlaybackParameters(mSpeedPlaybackParameters); 90 | } 91 | if (mSurface != null) 92 | mInternalPlayer.setVideoSurface(mSurface); 93 | 94 | if (mSubTitile != null) { 95 | MediaSource textMediaSource = getTextSource(Uri.parse(mSubTitile)); 96 | mMediaSource = new MergingMediaSource(mMediaSource, textMediaSource); 97 | } 98 | mInternalPlayer.setMediaSource(mMediaSource); 99 | mInternalPlayer.prepare(); 100 | mInternalPlayer.setPlayWhenReady(false); 101 | } 102 | } 103 | ); 104 | } 105 | 106 | public MediaSource getTextSource(Uri subTitle) { 107 | //todo C.SELECTION_FLAG_AUTOSELECT language MimeTypes 108 | Format textFormat = new Format.Builder() 109 | /// 其他的比如 text/x-ssa ,text/vtt,application/ttml+xml 等等 110 | .setSampleMimeType(MimeTypes.APPLICATION_SUBRIP) 111 | .setSelectionFlags(C.SELECTION_FLAG_FORCED) 112 | /// 如果出现字幕不显示,可以通过修改这个语音去对应, 113 | // 这个问题在内部的 selectTextTrack 时,TextTrackScore 通过 getFormatLanguageScore 方法判断语言获取匹配不上 114 | // 就会不出现字幕 115 | .setLanguage("en") 116 | .build(); 117 | 118 | MediaItem.SubtitleConfiguration subtitle = new MediaItem.SubtitleConfiguration.Builder(subTitle) 119 | .setMimeType(checkNotNull(textFormat.sampleMimeType)) 120 | .setLanguage( textFormat.language) 121 | .setSelectionFlags(textFormat.selectionFlags).build(); 122 | 123 | DefaultHttpDataSource.Factory factory = new DefaultHttpDataSource.Factory() 124 | .setAllowCrossProtocolRedirects(true) 125 | .setConnectTimeoutMs(50000) 126 | .setReadTimeoutMs(50000) 127 | .setTransferListener( new DefaultBandwidthMeter.Builder(mAppContext).build()); 128 | 129 | MediaSource textMediaSource = new SingleSampleMediaSource.Factory(new DefaultDataSource.Factory(mAppContext, 130 | factory)) 131 | .createMediaSource(subtitle, C.TIME_UNSET); 132 | return textMediaSource; 133 | 134 | } 135 | 136 | 137 | public String getSubTitile() { 138 | return mSubTitile; 139 | } 140 | 141 | public void setSubTitile(String subTitile) { 142 | this.mSubTitile = subTitile; 143 | } 144 | 145 | public Player.Listener getTextOutput() { 146 | return mTextOutput; 147 | } 148 | 149 | public void setTextOutput(Player.Listener textOutput) { 150 | this.mTextOutput = textOutput; 151 | } 152 | 153 | public void addTextOutputPlaying(Player.Listener textOutput) { 154 | if (mInternalPlayer != null) { 155 | mInternalPlayer.addListener(textOutput); 156 | } 157 | } 158 | 159 | public void removeTextOutput(Player.Listener textOutput) { 160 | if (mInternalPlayer != null) { 161 | mInternalPlayer.removeListener(textOutput); 162 | } 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/widget/subtitle/GSYExoSubTitlePlayerManager.java: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.widget.subtitle; 2 | 3 | import android.content.Context; 4 | import android.media.AudioManager; 5 | import android.net.TrafficStats; 6 | import android.net.Uri; 7 | import android.os.Message; 8 | import android.view.Surface; 9 | 10 | import androidx.annotation.Nullable; 11 | 12 | import com.google.android.exoplayer2.Player; 13 | import com.google.android.exoplayer2.SeekParameters; 14 | import com.google.android.exoplayer2.video.PlaceholderSurface; 15 | import com.shuyu.gsyvideoplayer.cache.ICacheManager; 16 | import com.shuyu.gsyvideoplayer.model.VideoOptionModel; 17 | import com.shuyu.gsyvideoplayer.player.BasePlayerManager; 18 | 19 | import java.util.List; 20 | 21 | import tv.danmaku.ijk.media.player.IMediaPlayer; 22 | 23 | /** 24 | * Created by guoshuyu on 2018/5/16. 25 | * 自定义player管理器,装载自定义exo player,实现无缝切换效果 26 | */ 27 | public class GSYExoSubTitlePlayerManager extends BasePlayerManager { 28 | 29 | private Context context; 30 | 31 | private GSYExoSubTitlePlayer mediaPlayer; 32 | 33 | private Surface surface; 34 | 35 | private PlaceholderSurface dummySurface; 36 | 37 | private long lastTotalRxBytes = 0; 38 | 39 | private long lastTimeStamp = 0; 40 | 41 | @Override 42 | public IMediaPlayer getMediaPlayer() { 43 | return mediaPlayer; 44 | } 45 | 46 | @Override 47 | public void initVideoPlayer(Context context, Message msg, List optionModelList, ICacheManager cacheManager) { 48 | this.context = context.getApplicationContext(); 49 | mediaPlayer = new GSYExoSubTitlePlayer(context); 50 | mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 51 | if (dummySurface == null) { 52 | dummySurface = PlaceholderSurface.newInstanceV17(context, false); 53 | } 54 | //使用自己的cache模式 55 | GSYExoSubTitleModel gsyModel = (GSYExoSubTitleModel) msg.obj; 56 | try { 57 | mediaPlayer.setLooping(gsyModel.isLooping()); 58 | if (gsyModel.getSubTitle() != null) { 59 | mediaPlayer.setSubTitile(gsyModel.getSubTitle()); 60 | } 61 | mediaPlayer.setPreview(gsyModel.getMapHeadData() != null && gsyModel.getMapHeadData().size() > 0); 62 | if (gsyModel.isCache() && cacheManager != null) { 63 | //通过管理器处理 64 | cacheManager.doCacheLogic(context, mediaPlayer, gsyModel.getUrl(), gsyModel.getMapHeadData(), gsyModel.getCachePath()); 65 | } else { 66 | //通过自己的内部缓存机制 67 | mediaPlayer.setCache(gsyModel.isCache()); 68 | mediaPlayer.setCacheDir(gsyModel.getCachePath()); 69 | mediaPlayer.setOverrideExtension(gsyModel.getOverrideExtension()); 70 | mediaPlayer.setDataSource(context, Uri.parse(gsyModel.getUrl()), gsyModel.getMapHeadData()); 71 | } 72 | if (gsyModel.getSpeed() != 1 && gsyModel.getSpeed() > 0) { 73 | mediaPlayer.setSpeed(gsyModel.getSpeed(), 1); 74 | } 75 | if (gsyModel.getTextOutput() != null) { 76 | mediaPlayer.setTextOutput(gsyModel.getTextOutput()); 77 | } 78 | } catch (Exception e) { 79 | e.printStackTrace(); 80 | } 81 | initSuccess(gsyModel); 82 | } 83 | 84 | @Override 85 | public void showDisplay(final Message msg) { 86 | if (mediaPlayer == null) { 87 | return; 88 | } 89 | if (msg.obj == null) { 90 | mediaPlayer.setSurface(dummySurface); 91 | } else { 92 | Surface holder = (Surface) msg.obj; 93 | surface = holder; 94 | mediaPlayer.setSurface(holder); 95 | } 96 | } 97 | 98 | @Override 99 | public void setSpeed(final float speed, final boolean soundTouch) { 100 | if (mediaPlayer != null) { 101 | try { 102 | mediaPlayer.setSpeed(speed, 1); 103 | } catch (Exception e) { 104 | e.printStackTrace(); 105 | } 106 | } 107 | } 108 | 109 | @Override 110 | public void setNeedMute(final boolean needMute) { 111 | if (mediaPlayer != null) { 112 | if (needMute) { 113 | mediaPlayer.setVolume(0, 0); 114 | } else { 115 | mediaPlayer.setVolume(1, 1); 116 | } 117 | } 118 | } 119 | 120 | @Override 121 | public void setVolume(float left, float right) { 122 | if (mediaPlayer != null) { 123 | mediaPlayer.setVolume(left, right); 124 | } 125 | } 126 | 127 | 128 | @Override 129 | public void releaseSurface() { 130 | if (surface != null) { 131 | //surface.release(); 132 | surface = null; 133 | } 134 | } 135 | 136 | @Override 137 | public void release() { 138 | if (mediaPlayer != null) { 139 | mediaPlayer.setSurface(null); 140 | mediaPlayer.release(); 141 | } 142 | if (dummySurface != null) { 143 | dummySurface.release(); 144 | dummySurface = null; 145 | } 146 | lastTotalRxBytes = 0; 147 | lastTimeStamp = 0; 148 | } 149 | 150 | @Override 151 | public int getBufferedPercentage() { 152 | if (mediaPlayer != null) { 153 | return mediaPlayer.getBufferedPercentage(); 154 | } 155 | return 0; 156 | } 157 | 158 | @Override 159 | public long getNetSpeed() { 160 | if (mediaPlayer != null) { 161 | return getNetSpeed(context); 162 | } 163 | return 0; 164 | } 165 | 166 | 167 | @Override 168 | public void setSpeedPlaying(float speed, boolean soundTouch) { 169 | 170 | } 171 | 172 | 173 | @Override 174 | public void start() { 175 | if (mediaPlayer != null) { 176 | mediaPlayer.start(); 177 | } 178 | } 179 | 180 | @Override 181 | public void stop() { 182 | if (mediaPlayer != null) { 183 | mediaPlayer.stop(); 184 | } 185 | } 186 | 187 | @Override 188 | public void pause() { 189 | if (mediaPlayer != null) { 190 | mediaPlayer.pause(); 191 | } 192 | } 193 | 194 | @Override 195 | public int getVideoWidth() { 196 | if (mediaPlayer != null) { 197 | return mediaPlayer.getVideoWidth(); 198 | } 199 | return 0; 200 | } 201 | 202 | @Override 203 | public int getVideoHeight() { 204 | if (mediaPlayer != null) { 205 | return mediaPlayer.getVideoHeight(); 206 | } 207 | return 0; 208 | } 209 | 210 | @Override 211 | public boolean isPlaying() { 212 | if (mediaPlayer != null) { 213 | return mediaPlayer.isPlaying(); 214 | } 215 | return false; 216 | } 217 | 218 | @Override 219 | public void seekTo(long time) { 220 | if (mediaPlayer != null) { 221 | mediaPlayer.seekTo(time); 222 | } 223 | } 224 | 225 | @Override 226 | public long getCurrentPosition() { 227 | if (mediaPlayer != null) { 228 | return mediaPlayer.getCurrentPosition(); 229 | } 230 | return 0; 231 | } 232 | 233 | @Override 234 | public long getDuration() { 235 | if (mediaPlayer != null) { 236 | return mediaPlayer.getDuration(); 237 | } 238 | return 0; 239 | } 240 | 241 | @Override 242 | public int getVideoSarNum() { 243 | if (mediaPlayer != null) { 244 | return mediaPlayer.getVideoSarNum(); 245 | } 246 | return 1; 247 | } 248 | 249 | @Override 250 | public int getVideoSarDen() { 251 | if (mediaPlayer != null) { 252 | return mediaPlayer.getVideoSarDen(); 253 | } 254 | return 1; 255 | } 256 | 257 | @Override 258 | public boolean isSurfaceSupportLockCanvas() { 259 | return false; 260 | } 261 | 262 | public void addTextOutputPlaying(Player.Listener textOutput) { 263 | if(mediaPlayer != null) { 264 | mediaPlayer.addTextOutputPlaying(textOutput); 265 | } 266 | } 267 | 268 | public void removeTextOutput(Player.Listener textOutput) { 269 | if(mediaPlayer != null) { 270 | mediaPlayer.removeTextOutput(textOutput); 271 | } 272 | } 273 | 274 | /** 275 | * 设置seek 的临近帧。 276 | **/ 277 | public void setSeekParameter(@Nullable SeekParameters seekParameters) { 278 | if (mediaPlayer != null) { 279 | mediaPlayer.setSeekParameter(seekParameters); 280 | } 281 | } 282 | 283 | 284 | private long getNetSpeed(Context context) { 285 | if (context == null) { 286 | return 0; 287 | } 288 | long nowTotalRxBytes = TrafficStats.getUidRxBytes(context.getApplicationInfo().uid) == TrafficStats.UNSUPPORTED ? 0 : (TrafficStats.getTotalRxBytes() / 1024);//转为KB 289 | long nowTimeStamp = System.currentTimeMillis(); 290 | long calculationTime = (nowTimeStamp - lastTimeStamp); 291 | if (calculationTime == 0) { 292 | return calculationTime; 293 | } 294 | //毫秒转换 295 | long speed = ((nowTotalRxBytes - lastTotalRxBytes) * 1000 / calculationTime); 296 | lastTimeStamp = nowTimeStamp; 297 | lastTotalRxBytes = nowTotalRxBytes; 298 | return speed; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxr/video_player/widget/subtitle/GSYExoSubTitleVideoManager.java: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player.widget.subtitle; 2 | 3 | import static com.shuyu.gsyvideoplayer.utils.CommonUtil.hideNavKey; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.app.Activity; 7 | import android.content.Context; 8 | import android.os.Message; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.view.Window; 12 | 13 | import com.google.android.exoplayer2.Player; 14 | import com.shuyu.gsyvideoplayer.GSYVideoBaseManager; 15 | import com.shuyu.gsyvideoplayer.player.IPlayerManager; 16 | import com.shuyu.gsyvideoplayer.utils.CommonUtil; 17 | import com.shuyu.gsyvideoplayer.video.base.GSYVideoPlayer; 18 | 19 | import java.io.File; 20 | import java.util.Map; 21 | 22 | public class GSYExoSubTitleVideoManager extends GSYVideoBaseManager { 23 | 24 | public static final int SMALL_ID = com.shuyu.gsyvideoplayer.R.id.small_id; 25 | 26 | public static final int FULLSCREEN_ID = com.shuyu.gsyvideoplayer.R.id.full_id; 27 | 28 | public static String TAG = "GSYExoVideoManager"; 29 | 30 | @SuppressLint("StaticFieldLeak") 31 | private static GSYExoSubTitleVideoManager videoManager; 32 | 33 | 34 | private GSYExoSubTitleVideoManager() { 35 | init(); 36 | } 37 | 38 | /** 39 | * 单例管理器 40 | */ 41 | public static synchronized GSYExoSubTitleVideoManager instance() { 42 | if (videoManager == null) { 43 | videoManager = new GSYExoSubTitleVideoManager(); 44 | } 45 | return videoManager; 46 | } 47 | 48 | @Override 49 | protected IPlayerManager getPlayManager() { 50 | playerManager = new GSYExoSubTitlePlayerManager(); 51 | return playerManager; 52 | } 53 | 54 | public void prepare(String url, String subTitle, Player.Listener textOutput, Map mapHeadData, boolean loop, float speed, boolean cache, File cachePath, String overrideExtension) { 55 | Message msg = new Message(); 56 | msg.what = HANDLER_PREPARE; 57 | msg.obj = new GSYExoSubTitleModel(url, subTitle, textOutput, mapHeadData, loop, speed, cache, cachePath, overrideExtension); 58 | sendMessage(msg); 59 | } 60 | 61 | 62 | /** 63 | * 上一集 64 | */ 65 | public void previous() { 66 | if (playerManager == null) { 67 | return; 68 | } 69 | ((GSYExoSubTitleVideoManager) playerManager).previous(); 70 | } 71 | 72 | /** 73 | * 下一集 74 | */ 75 | public void next() { 76 | if (playerManager == null) { 77 | return; 78 | } 79 | ((GSYExoSubTitleVideoManager) playerManager).next(); 80 | } 81 | 82 | /** 83 | * 退出全屏,主要用于返回键 84 | * 85 | * @return 返回是否全屏 86 | */ 87 | @SuppressWarnings("ResourceType") 88 | public static boolean backFromWindowFull(Context context) { 89 | boolean backFrom = false; 90 | ViewGroup vp = (ViewGroup) (CommonUtil.scanForActivity(context)).findViewById(Window.ID_ANDROID_CONTENT); 91 | View oldF = vp.findViewById(FULLSCREEN_ID); 92 | if (oldF != null) { 93 | backFrom = true; 94 | hideNavKey(context); 95 | if (GSYExoSubTitleVideoManager.instance().lastListener() != null) { 96 | GSYExoSubTitleVideoManager.instance().lastListener().onBackFullscreen(); 97 | } 98 | } 99 | return backFrom; 100 | } 101 | 102 | /** 103 | * 页面销毁了记得调用是否所有的video 104 | */ 105 | public static void releaseAllVideos() { 106 | if (GSYExoSubTitleVideoManager.instance().listener() != null) { 107 | GSYExoSubTitleVideoManager.instance().listener().onCompletion(); 108 | } 109 | GSYExoSubTitleVideoManager.instance().releaseMediaPlayer(); 110 | } 111 | 112 | 113 | /** 114 | * 暂停播放 115 | */ 116 | public static void onPause() { 117 | if (GSYExoSubTitleVideoManager.instance().listener() != null) { 118 | GSYExoSubTitleVideoManager.instance().listener().onVideoPause(); 119 | } 120 | } 121 | 122 | /** 123 | * 恢复播放 124 | */ 125 | public static void onResume() { 126 | if (GSYExoSubTitleVideoManager.instance().listener() != null) { 127 | GSYExoSubTitleVideoManager.instance().listener().onVideoResume(); 128 | } 129 | } 130 | 131 | 132 | /** 133 | * 恢复暂停状态 134 | * 135 | * @param seek 是否产生seek动作,直播设置为false 136 | */ 137 | public static void onResume(boolean seek) { 138 | if (GSYExoSubTitleVideoManager.instance().listener() != null) { 139 | GSYExoSubTitleVideoManager.instance().listener().onVideoResume(seek); 140 | } 141 | } 142 | 143 | /** 144 | * 当前是否全屏状态 145 | * 146 | * @return 当前是否全屏状态, true代表是。 147 | */ 148 | @SuppressWarnings("ResourceType") 149 | public static boolean isFullState(Activity activity) { 150 | ViewGroup vp = (ViewGroup) (CommonUtil.scanForActivity(activity)).findViewById(Window.ID_ANDROID_CONTENT); 151 | final View full = vp.findViewById(FULLSCREEN_ID); 152 | GSYVideoPlayer gsyVideoPlayer = null; 153 | if (full != null) { 154 | gsyVideoPlayer = (GSYVideoPlayer) full; 155 | } 156 | return gsyVideoPlayer != null; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_home.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/app/src/main/res/drawable-xxhdpi/ic_home.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_multi_choice.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/app/src/main/res/drawable-xxhdpi/ic_multi_choice.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_setting.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/app/src/main/res/drawable-xxhdpi/ic_setting.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/iv_splash.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/app/src/main/res/drawable-xxhdpi/iv_splash.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/iv_video.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/app/src/main/res/drawable-xxhdpi/iv_video.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_progressbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bottom_navigation_item_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_round_pause_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_round_play_arrow_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_round_skip_next_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_round_skip_previous_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_subtitles_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_player.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 24 | 25 | 37 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_simple_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_video_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 23 | 24 | 32 | 38 | 39 | 40 | 51 | 52 | 61 | 71 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_movie_folder_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 26 | 27 | 32 | 33 | 39 | 40 | 45 | 46 | 47 | 48 | 54 | 55 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 20 | 21 | 25 | 26 | 31 | 32 | 39 | 40 | 47 | 48 | 56 | 57 | 58 | 59 | 66 | 67 | 73 | 81 | 82 | 83 | 90 | 91 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_folder.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 34 | 35 | 40 | 41 | 42 | 43 | 44 | 61 | 62 | 63 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_play_history.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 34 | 35 | 38 | 39 | 52 | 53 | 58 | 59 | 66 | 67 | 68 | 69 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | 35 | 36 | 39 | 40 | 45 | 46 | 53 | 54 | 55 | 56 | 57 | 58 | 75 | 76 | 87 | 88 | 99 | 100 | 112 | 113 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /app/src/main/res/layout/video_layout_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 14 | 15 | 27 | 28 | 37 | 38 | 45 | 46 | 60 | 61 | 68 | 69 | 76 | 77 | 78 | 86 | 87 | 94 | 95 | 96 | 104 | 105 | 106 | 114 | 115 | 116 | 125 | 126 | 136 | 137 | 146 | 147 | 154 | 155 | 163 | 164 | 165 | 166 | 174 | 175 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_custom_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 16 | 17 | 23 | 24 | 36 | 37 | 47 | 48 | 55 | 62 | 63 | 77 | 78 | 85 | 92 | 99 | 100 | 101 | 108 | 115 | 116 | 117 | 118 | 119 | 127 | 128 | 135 | 136 | 143 | 144 | 150 | 151 | 152 | 161 | 162 | 172 | 173 | 182 | 183 | 190 | 191 | 199 | 200 | 205 | 206 | 211 | 212 | 213 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /app/src/main/res/menu/tabs_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #292635 5 | 6 | #ef9608 7 | 8 | #000 9 | 10 | 11 | #FFFFFF 12 | #000000 13 | #e6e6e6 14 | 15 | #ffcb6b 16 | #ff4444 17 | 18 | #b3b3b3 19 | 20 | 21 | 22 | #000000 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 150dp 4 | 6dp 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 涵涵播放器 3 | 首页 4 | 设置 5 | 6 | IJK 内核 7 | EXO 内核 8 | 系统 内核 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 19 | 20 | 21 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/test/java/com/lxr/video_player/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.lxr.video_player 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id 'com.android.application' version '7.2.2' apply false 4 | id 'com.android.library' version '7.2.2' apply false 5 | id 'org.jetbrains.kotlin.android' version '1.7.10' apply false 6 | } 7 | 8 | task clean(type: Delete) { 9 | delete rootProject.buildDir 10 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.enableJetifier = true 25 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jan 06 17:31:59 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /hh.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/hh.jks -------------------------------------------------------------------------------- /screenshot/main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/screenshot/main.jpg -------------------------------------------------------------------------------- /screenshot/player.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/screenshot/player.jpg -------------------------------------------------------------------------------- /screenshot/setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/screenshot/setting.jpg -------------------------------------------------------------------------------- /screenshot/video_list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XiaoRanLiu3119/hanhan_video_player/ea138bac6908d7d91d952254ec63d65a3d825717/screenshot/video_list.jpg -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { url 'https://jitpack.io' } 14 | maven { url "https://maven.aliyun.com/repository/public" } 15 | } 16 | } 17 | rootProject.name = "hanhan_video_player" 18 | include ':app' 19 | --------------------------------------------------------------------------------