├── .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 | 
13 | 
14 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------