├── .gitignore ├── build.gradle ├── gradle.properties ├── gradlew ├── gradlew.bat ├── readme.md ├── settings.gradle └── subtitlelibrary ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── zlm │ └── subtitlelibrary │ ├── SubtitleReader.java │ ├── entity │ ├── SubtitleInfo.java │ └── SubtitleLineInfo.java │ ├── formats │ ├── SubtitleFileReader.java │ ├── SubtitleFileWriter.java │ ├── ass │ │ └── AssSubtitleFileReader.java │ └── srt │ │ ├── SrtSubtitleFileReader.java │ │ └── SrtSubtitleFileWriter.java │ ├── util │ ├── FileUtil.java │ ├── SubtitleUtil.java │ └── TimeUtil.java │ └── widget │ └── SubtitleView.java └── res └── values └── strings.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.1.4' 11 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | maven { url 'https://jitpack.io' } 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } 29 | -------------------------------------------------------------------------------- /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=-Xmx1536m 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 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 简介 # 2 | [乐乐音乐播放器](https://github.com/zhangliangming/HappyPlayer5.git)最近添加了MV功能,由于部分MV没有字幕,所以这里为[乐乐音乐播放器](https://github.com/zhangliangming/HappyPlayer5.git)添加一个外挂字幕的开源库,目前该开源库主要简单支持srt和ass字幕文件。 3 | 4 | # 字幕显示方式 # 5 | 6 | {\fn华文楷体\fs16\1c&H3CF1F3&\b1}影片壓制 7 | 8 | 转换 9 | 10 | ` 影片壓制` 11 | 12 | 显示方式主要是以html的方式显示,所以现在只支持读取字幕文本、html文本、颜色和加粗等基本功能,没有特效。 13 | 14 | # 字幕格式解析 # 15 | 16 | ## 正则表达式 ## 17 | - 时间标签 18 | 19 | `\d+:\d+:\d+,\d+` 20 | 21 | 22 | - 分隔出每一项font标签 23 | 24 | 25 | 26 | (\)(\<[bius]\>)*[^\<]+(\)*(\) 27 | 28 | - 分隔出字幕内容 29 | 30 | ` http://cmct.cc` 31 | 32 | ((\)(\<[bius]\>)*|(\)*(\)) 33 | 34 | - 分隔ass 35 | 36 | 37 | Dialogue: 0,0:00:02.00,0:00:07.00,Default,,0000,0000,0001,,{\fn华文楷体\fs16\1c&H3CF1F3&\b0}--==本影片由 {\1c&HFF8000&\b1}CMCT 团队{\fn华文楷体\1c&H3CF1F3&\b0} 荣誉出品==--\N更多精彩影视 请访问 {\fnCronos Pro Subhead\1c&HFF00FF&\b1}http://cmct.cc{\r} 38 | 39 | Dialogue\S\s+\d+,\d+:\d+:\d+.\d+,\d+:\d+:\d+.\d+,\S+, 40 | 41 | 42 | ## srt字幕 ## 43 | 44 | [SRT字幕的颜色以及一些特效的设置](http://www.360doc.com/content/17/0527/14/57493_657716572.shtml) 45 | 46 | 47 | ## ass字幕 ## 48 | [ASS字幕格式规范](https://www.douban.com/note/658520175/) 49 | 50 | # 日志 # 51 | ## 2019-01-17 ## 52 | - 添加字幕预览视图 53 | 54 | 55 | # 预览图 # 56 | ## srt字幕 ## 57 | ![](https://i.imgur.com/SQMQBok.png) 58 | 59 | ![](https://i.imgur.com/P4WqgeC.png) 60 | 61 | ## ass字幕 ## 62 | ![](https://i.imgur.com/MQ5xnUW.png) 63 | 64 | ![](https://i.imgur.com/Fdvnjh2.png) 65 | 66 | # Gradle # 67 | 68 | 1.root build.gradle 69 | 70 | `allprojects { 71 | repositories { 72 | ... 73 | maven { url 'https://jitpack.io' } 74 | } 75 | }` 76 | 77 | 2.app build.gradle 78 | 79 | `dependencies { 80 | implementation 'com.github.zhangliangming:Subtitle:v1.2' 81 | }` 82 | 83 | # 混淆注意 # 84 | -keep class com.zlm.subtitlelibrary.** { *; } 85 | 86 | # 调用Demo # 87 | 88 | 链接: https://pan.baidu.com/s/1j-4wbtiNIfRhypb4uEnX6g 提取码: t8dj 89 | 90 | # 声明 # 91 | 92 | 该项目的代码和内容仅用于学习用途 93 | 94 | # 捐赠 # 95 | 96 | 如果该项目对您有所帮助,欢迎您的赞赏 97 | 98 | - 微信 99 | 100 | ![](https://i.imgur.com/hOs6tPn.png) 101 | 102 | - 支付宝 103 | 104 | ![](https://i.imgur.com/DGB9Lq0.png) -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':subtitlelibrary' 2 | -------------------------------------------------------------------------------- /subtitlelibrary/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /subtitlelibrary/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | group = 'com.github.zhangliangming' 4 | 5 | android { 6 | compileSdkVersion 28 7 | 8 | 9 | 10 | defaultConfig { 11 | minSdkVersion 21 12 | targetSdkVersion 26 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 17 | 18 | } 19 | 20 | buildTypes { 21 | release { 22 | zipAlignEnabled true 23 | minifyEnabled true 24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | } 29 | 30 | dependencies { 31 | implementation fileTree(dir: 'libs', include: ['*.jar']) 32 | 33 | implementation 'com.android.support:appcompat-v7:28.0.0' 34 | testImplementation 'junit:junit:4.12' 35 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 36 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 37 | } 38 | -------------------------------------------------------------------------------- /subtitlelibrary/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 22 | 23 | -keep class com.zlm.subtitlelibrary.** 24 | -keepclassmembers class com.zlm.subtitlelibrary.** { 25 | public *; 26 | } 27 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/SubtitleReader.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary; 2 | 3 | import com.zlm.subtitlelibrary.entity.SubtitleInfo; 4 | import com.zlm.subtitlelibrary.formats.SubtitleFileReader; 5 | import com.zlm.subtitlelibrary.util.SubtitleUtil; 6 | 7 | import java.io.File; 8 | 9 | /** 10 | * @Description: 字体读取类 11 | * @author: zhangliangming 12 | * @date: 2019-01-13 16:44 13 | **/ 14 | public class SubtitleReader { 15 | /** 16 | * 时间补偿值,其单位是毫秒,正值表示整体提前,负值相反。这是用于总体调整显示快慢的。 17 | */ 18 | private long defOffset = 0; 19 | /** 20 | * 增量 21 | */ 22 | private long offset = 0; 23 | 24 | /** 25 | * 文件路径 26 | */ 27 | private String filePath; 28 | 29 | /** 30 | * 文件hash 31 | */ 32 | private String hash; 33 | 34 | /** 35 | * 字幕数据集合 36 | */ 37 | private SubtitleInfo subtitleInfo; 38 | 39 | public SubtitleReader() { 40 | 41 | } 42 | 43 | /** 44 | * @throws 45 | * @Description: 读取字幕文件 46 | * @param: 47 | * @return: 48 | * @author: zhangliangming 49 | * @date: 2019-01-12 19:12 50 | */ 51 | public void readFile(File file) throws Exception { 52 | if (file != null) { 53 | filePath = file.getPath(); 54 | SubtitleFileReader subtitleFileReader = SubtitleUtil.getSubtitleFileReader(file); 55 | subtitleInfo = subtitleFileReader.readFile(file); 56 | } 57 | } 58 | 59 | /** 60 | * 读取字幕内容并保存到文件 61 | * 62 | * @param fileContentString 63 | * @param saveFile 不能为空 64 | * @throws Exception 65 | */ 66 | public void readText(String fileContentString, File saveFile) throws Exception { 67 | if (saveFile != null) { 68 | filePath = saveFile.getPath(); 69 | SubtitleFileReader subtitleFileReader = SubtitleUtil.getSubtitleFileReader(saveFile); 70 | subtitleInfo = subtitleFileReader.readText(fileContentString, saveFile); 71 | } 72 | } 73 | 74 | /** 75 | * 播放的时间补偿值 76 | * 77 | * @return 78 | */ 79 | public long getPlayOffset() { 80 | return defOffset + offset; 81 | } 82 | 83 | public long getOffset() { 84 | return offset; 85 | } 86 | 87 | public void setOffset(long offset) { 88 | this.offset = offset; 89 | } 90 | 91 | public String getFilePath() { 92 | return filePath; 93 | } 94 | 95 | public void setFilePath(String filePath) { 96 | this.filePath = filePath; 97 | } 98 | 99 | public String getHash() { 100 | return hash; 101 | } 102 | 103 | public void setHash(String hash) { 104 | this.hash = hash; 105 | } 106 | 107 | public SubtitleInfo getSubtitleInfo() { 108 | return subtitleInfo; 109 | } 110 | 111 | public void setSubtitleInfo(SubtitleInfo subtitleInfo) { 112 | this.subtitleInfo = subtitleInfo; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/entity/SubtitleInfo.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.entity; 2 | 3 | import java.nio.charset.Charset; 4 | import java.util.List; 5 | 6 | /** 7 | * @Description: 字幕集合 8 | * @author: zhangliangming 9 | * @date: 2019-01-12 15:56 10 | **/ 11 | public class SubtitleInfo { 12 | private String ext; 13 | private Charset defaultCharset; 14 | private List subtitleLineInfos; 15 | 16 | public String getExt() { 17 | return ext; 18 | } 19 | 20 | public void setExt(String ext) { 21 | this.ext = ext; 22 | } 23 | 24 | public Charset getDefaultCharset() { 25 | return defaultCharset; 26 | } 27 | 28 | public void setDefaultCharset(Charset defaultCharset) { 29 | this.defaultCharset = defaultCharset; 30 | } 31 | 32 | public List getSubtitleLineInfos() { 33 | return subtitleLineInfos; 34 | } 35 | 36 | public void setSubtitleLineInfos(List subtitleLineInfos) { 37 | this.subtitleLineInfos = subtitleLineInfos; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/entity/SubtitleLineInfo.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.entity; 2 | 3 | /** 4 | * @Description: 字幕行数据 5 | * @author: zhangliangming 6 | * @date: 2019-01-12 15:55 7 | **/ 8 | public class SubtitleLineInfo { 9 | /** 10 | * 开始时间 11 | */ 12 | private int startTime; 13 | /** 14 | * 结束时间 15 | */ 16 | private int endTime; 17 | 18 | /** 19 | * 字幕内容 20 | */ 21 | private String subtitleText; 22 | 23 | /** 24 | * 样式字幕内容 25 | */ 26 | private String subtitleHtml; 27 | 28 | public int getStartTime() { 29 | return startTime; 30 | } 31 | 32 | public void setStartTime(int startTime) { 33 | this.startTime = startTime; 34 | } 35 | 36 | public int getEndTime() { 37 | return endTime; 38 | } 39 | 40 | public void setEndTime(int endTime) { 41 | this.endTime = endTime; 42 | } 43 | 44 | public String getSubtitleText() { 45 | return subtitleText; 46 | } 47 | 48 | public void setSubtitleText(String subtitleText) { 49 | this.subtitleText = subtitleText.replaceAll("\r", ""); 50 | } 51 | 52 | public String getSubtitleHtml() { 53 | return subtitleHtml; 54 | } 55 | 56 | public void setSubtitleHtml(String subtitleHtml) { 57 | this.subtitleHtml = subtitleHtml.replaceAll("\r|\n", ""); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/formats/SubtitleFileReader.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.formats; 2 | 3 | import com.zlm.subtitlelibrary.entity.SubtitleInfo; 4 | 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.io.InputStream; 9 | import java.io.OutputStreamWriter; 10 | import java.io.PrintWriter; 11 | import java.nio.charset.Charset; 12 | 13 | /** 14 | * @Description: 字幕文件读取器 15 | * @author: zhangliangming 16 | * @date: 2019-01-12 15:53 17 | **/ 18 | public abstract class SubtitleFileReader { 19 | 20 | /** 21 | * 默认编码 22 | */ 23 | private Charset defaultCharset = Charset.forName("utf-8"); 24 | 25 | /** 26 | * @throws 27 | * @Description: 读取字幕文件 28 | * @param: 29 | * @return: 30 | * @author: zhangliangming 31 | * @date: 2019-01-12 19:12 32 | */ 33 | public SubtitleInfo readFile(File file) throws Exception { 34 | if (file != null) { 35 | return readInputStream(new FileInputStream(file)); 36 | } 37 | return null; 38 | } 39 | 40 | /** 41 | * @throws 42 | * @Description: 读取文件流 43 | * @param: 44 | * @return: 45 | * @author: zhangliangming 46 | * @date: 2019-01-12 19:28 47 | */ 48 | public abstract SubtitleInfo readInputStream(InputStream in) throws Exception; 49 | 50 | /** 51 | * @throws 52 | * @Description: 读取字幕文本 53 | * @param: saveFile 需要保存的字幕文件对象 54 | * @return: 55 | * @author: zhangliangming 56 | * @date: 2019-01-12 19:29 57 | */ 58 | public abstract SubtitleInfo readText(String fileContentString, File saveFile) throws Exception; 59 | 60 | 61 | /** 62 | * @throws 63 | * @Description: 保存文件 64 | * @param:saveFile 需要保存的字幕文件对象 65 | * @return: 66 | * @author: zhangliangming 67 | * @date: 2019-01-12 19:15 68 | *//**/ 69 | public boolean saveFile(File saveFile, String fileContentString) throws Exception { 70 | if (saveFile != null) { 71 | 72 | if (!saveFile.getParentFile().exists()) { 73 | saveFile.getParentFile().mkdirs(); 74 | } 75 | 76 | 77 | OutputStreamWriter outstream = new OutputStreamWriter( 78 | new FileOutputStream(saveFile), 79 | getDefaultCharset()); 80 | PrintWriter writer = new PrintWriter(outstream); 81 | writer.write(fileContentString); 82 | writer.close(); 83 | 84 | outstream = null; 85 | writer = null; 86 | 87 | return true; 88 | } 89 | 90 | return false; 91 | } 92 | 93 | /** 94 | * 支持文件格式 95 | * 96 | * @param ext 文件后缀名 97 | * @return 98 | */ 99 | public abstract boolean isFileSupported(String ext); 100 | 101 | /** 102 | * 获取支持的文件后缀名 103 | * 104 | * @return 105 | */ 106 | public abstract String getSupportFileExt(); 107 | 108 | public void setDefaultCharset(Charset charset) { 109 | defaultCharset = charset; 110 | } 111 | 112 | public Charset getDefaultCharset() { 113 | return defaultCharset; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/formats/SubtitleFileWriter.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.formats; 2 | 3 | import com.zlm.subtitlelibrary.entity.SubtitleInfo; 4 | 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.OutputStreamWriter; 8 | import java.io.PrintWriter; 9 | import java.nio.charset.Charset; 10 | 11 | /** 12 | * @Description: 字幕文件保存器 13 | * @author: zhangliangming 14 | * @date: 2019-01-12 15:54 15 | **/ 16 | public abstract class SubtitleFileWriter { 17 | /** 18 | * 默认编码 19 | */ 20 | private Charset defaultCharset = Charset.forName("utf-8"); 21 | 22 | /** 23 | * 保存文件 24 | * 25 | * @param subtitleInfo 26 | * @param filePath 27 | * @return 28 | * @throws Exception 29 | */ 30 | public abstract boolean writer(SubtitleInfo subtitleInfo, String filePath) 31 | throws Exception; 32 | 33 | 34 | /** 35 | * 保存文件 36 | * 37 | * @param subtitleContent 38 | * @param filePath 39 | * @return 40 | * @throws Exception 41 | */ 42 | public boolean saveFile(String subtitleContent, String filePath) throws Exception { 43 | 44 | File saveFile = new File(filePath); 45 | if (saveFile != null) { 46 | // 47 | if (!saveFile.getParentFile().exists()) { 48 | saveFile.getParentFile().mkdirs(); 49 | } 50 | OutputStreamWriter outstream = new OutputStreamWriter( 51 | new FileOutputStream(filePath), 52 | getDefaultCharset()); 53 | PrintWriter writer = new PrintWriter(outstream); 54 | writer.write(subtitleContent); 55 | writer.close(); 56 | 57 | outstream = null; 58 | writer = null; 59 | 60 | return true; 61 | } 62 | 63 | return false; 64 | } 65 | 66 | 67 | /** 68 | * 保存文件 69 | * 70 | * @param subtitleContent 71 | * @param filePath 72 | * @return 73 | * @throws Exception 74 | */ 75 | public boolean saveFile(byte[] subtitleContent, String filePath) throws Exception { 76 | 77 | File saveFile = new File(filePath); 78 | if (saveFile != null) { 79 | 80 | if (!saveFile.getParentFile().exists()) { 81 | saveFile.getParentFile().mkdirs(); 82 | } 83 | FileOutputStream os = new FileOutputStream(saveFile); 84 | os.write(subtitleContent); 85 | os.close(); 86 | 87 | os = null; 88 | 89 | return true; 90 | } 91 | return false; 92 | } 93 | 94 | /** 95 | * 获取字幕保存内容 96 | * 97 | * @param subtitleInfo 98 | * @return 99 | * @throws Exception 100 | */ 101 | public abstract String getSubtitleContent(SubtitleInfo subtitleInfo) throws Exception; 102 | 103 | 104 | /** 105 | * 支持文件格式 106 | * 107 | * @param ext 文件后缀名 108 | * @return 109 | */ 110 | public abstract boolean isFileSupported(String ext); 111 | 112 | /** 113 | * 获取支持的文件后缀名 114 | * 115 | * @return 116 | */ 117 | public abstract String getSupportFileExt(); 118 | 119 | public void setDefaultCharset(Charset charset) { 120 | defaultCharset = charset; 121 | } 122 | 123 | public Charset getDefaultCharset() { 124 | return defaultCharset; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/formats/ass/AssSubtitleFileReader.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.formats.ass; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.zlm.subtitlelibrary.entity.SubtitleInfo; 6 | import com.zlm.subtitlelibrary.entity.SubtitleLineInfo; 7 | import com.zlm.subtitlelibrary.formats.SubtitleFileReader; 8 | import com.zlm.subtitlelibrary.util.SubtitleUtil; 9 | import com.zlm.subtitlelibrary.util.TimeUtil; 10 | 11 | import java.io.BufferedReader; 12 | import java.io.File; 13 | import java.io.InputStream; 14 | import java.io.InputStreamReader; 15 | import java.util.ArrayList; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.regex.Matcher; 20 | import java.util.regex.Pattern; 21 | 22 | /** 23 | * @Description: ass字幕读取器 24 | * @author: zhangliangming 25 | * @date: 2019-01-14 0:11 26 | **/ 27 | public class AssSubtitleFileReader extends SubtitleFileReader { 28 | /** 29 | * 样式集合 30 | */ 31 | private Map mStyleMap = new HashMap(); 32 | 33 | @Override 34 | public SubtitleInfo readInputStream(InputStream in) throws Exception { 35 | if (in != null) { 36 | BufferedReader br = new BufferedReader(new InputStreamReader(in, 37 | getDefaultCharset())); 38 | StringBuilder fileContentSB = new StringBuilder(); 39 | String lineInfo = ""; 40 | while ((lineInfo = br.readLine()) != null) { 41 | fileContentSB.append(lineInfo + "\n"); 42 | } 43 | in.close(); 44 | br.close(); 45 | in = null; 46 | br = null; 47 | return readText(fileContentSB.toString(), null); 48 | } 49 | return null; 50 | } 51 | 52 | @Override 53 | public SubtitleInfo readText(String fileContentString, File saveFile) throws Exception { 54 | saveFile(saveFile, fileContentString); 55 | SubtitleInfo subtitleInfo = new SubtitleInfo(); 56 | subtitleInfo.setDefaultCharset(getDefaultCharset()); 57 | subtitleInfo.setExt(getSupportFileExt()); 58 | 59 | List subtitleLineInfos = new ArrayList(); 60 | 61 | String[] fileContents = fileContentString.split("\n"); 62 | 63 | for (int i = 0; i < fileContents.length; i++) { 64 | parseSubtitleInfo(fileContents[i], subtitleLineInfos); 65 | } 66 | 67 | //设置字幕 68 | if (subtitleLineInfos != null && subtitleLineInfos.size() > 0) { 69 | subtitleInfo.setSubtitleLineInfos(subtitleLineInfos); 70 | } 71 | 72 | return subtitleInfo; 73 | } 74 | 75 | /** 76 | * 解析字幕 77 | * 78 | * @param subtitleLineString 79 | * @param subtitleLineInfos 80 | */ 81 | private void parseSubtitleInfo(String subtitleLineString, List subtitleLineInfos) { 82 | if (subtitleLineString.startsWith("Style")) { 83 | parseStyle(subtitleLineString); 84 | } else if (subtitleLineString.startsWith("Dialogue")) { 85 | SubtitleLineInfo subtitleLineInfo = new SubtitleLineInfo(); 86 | boolean flag = parseSubtitleTime(subtitleLineString, subtitleLineInfo); 87 | if (!flag) return; 88 | 89 | String subtitleString = parseSubtitleString(subtitleLineString); 90 | //分隔每行字幕 91 | String[] splitSubtitles = subtitleString.split("\\\\[N]"); 92 | 93 | //加载字幕 94 | String subtitleHtmlString = ""; 95 | String subtitleTextString = ""; 96 | for (int i = 0; i < splitSubtitles.length; i++) { 97 | String temp = subtitleAddStyle(subtitleLineString, splitSubtitles[i]); 98 | String[] result = SubtitleUtil.parseSubtitleText(temp); 99 | subtitleTextString += result[0]; 100 | subtitleHtmlString += result[1]; 101 | 102 | if (i != splitSubtitles.length - 1) { 103 | subtitleTextString += "\n"; 104 | subtitleHtmlString += "
"; 105 | } 106 | } 107 | subtitleLineInfo.setSubtitleText(subtitleTextString); 108 | subtitleLineInfo.setSubtitleHtml(subtitleHtmlString); 109 | subtitleLineInfos.add(subtitleLineInfo); 110 | } 111 | } 112 | 113 | /** 114 | * 字幕内容添加文本样式 115 | * 116 | * @param subtitleLineString 117 | * @param subtitleString 118 | * @return 119 | */ 120 | private String subtitleAddStyle(String subtitleLineString, String subtitleString) { 121 | String styleName = getStyleName(subtitleLineString); 122 | if (!TextUtils.isEmpty(styleName)) { 123 | Style style = mStyleMap.get(styleName); 124 | if (style != null) { 125 | subtitleString = subtitleString.replaceAll("\\{\\r\\}", style.getStyleString()); 126 | 127 | //分隔出没有样式的字幕内容 128 | String regex = "\\{[^\\{]+\\}[^\\{]*"; 129 | String[] splitSubtitles = subtitleString.split(regex, -1); 130 | int index = 0; 131 | StringBuilder subtitleTextSB = new StringBuilder(); 132 | Pattern pattern = Pattern.compile(regex); 133 | Matcher matcher = pattern.matcher(subtitleString); 134 | //遍历样式字符串 135 | while (matcher.find()) { 136 | if (index == 0 && splitSubtitles.length > 0 && !TextUtils.isEmpty(splitSubtitles[0])) { 137 | subtitleTextSB.append(style.getStyleString() + splitSubtitles[0]); 138 | } 139 | String styleString = matcher.group(); 140 | if (index + 1 >= splitSubtitles.length) { 141 | break; 142 | } 143 | subtitleTextSB.append(styleString); 144 | 145 | index++; 146 | } 147 | //如果没有样式 148 | if (index == 0 && splitSubtitles.length > 0 && !TextUtils.isEmpty(splitSubtitles[0])) { 149 | subtitleTextSB.append(style.getStyleString() + splitSubtitles[0]); 150 | } 151 | //添加剩余的字幕内容 152 | for (index++; index < splitSubtitles.length; index++) { 153 | if (!TextUtils.isEmpty(splitSubtitles[index])) { 154 | subtitleTextSB.append(style.getStyleString() + splitSubtitles[index]); 155 | } 156 | } 157 | subtitleString = subtitleTextSB.toString(); 158 | 159 | } 160 | } 161 | return subtitleString; 162 | } 163 | 164 | /** 165 | * 获取样式名称 166 | * 167 | * @param subtitleLineString 168 | * @return 169 | */ 170 | private String getStyleName(String subtitleLineString) { 171 | String regex = "Dialogue\\S\\s+\\d+,\\d+:\\d+:\\d+.\\d+,\\d+:\\d+:\\d+.\\d+,\\S+,"; 172 | Pattern pattern = Pattern.compile(regex); 173 | Matcher matcher = pattern.matcher(subtitleLineString); 174 | if (matcher.find()) { 175 | String group = matcher.group(); 176 | String[] splitGroupString = group.split(","); 177 | return splitGroupString[3]; 178 | } 179 | return null; 180 | } 181 | 182 | /** 183 | * 解析style样式 184 | * 185 | * @param styleString 186 | */ 187 | private void parseStyle(String styleString) { 188 | styleString = styleString.replaceAll("Style\\S\\s+", ""); 189 | String[] splitStyles = styleString.split(","); 190 | 191 | Style style = new Style(); 192 | style.setName(splitStyles[0]); 193 | style.setFontname(splitStyles[1]); 194 | style.setFontsize(splitStyles[2]); 195 | style.setPrimaryColour(splitStyles[3]); 196 | style.setBold(splitStyles[7]); 197 | style.setItalic(splitStyles[8]); 198 | style.setUnderline(splitStyles[9]); 199 | style.setStrikeout(splitStyles[10]); 200 | 201 | mStyleMap.put(style.getName(), style); 202 | } 203 | 204 | /** 205 | * 解析歌词 206 | * 207 | * @param subtitleLineString 208 | * @return 209 | */ 210 | private String parseSubtitleString(String subtitleLineString) { 211 | String regex = "Dialogue\\S\\s+\\d+,\\d+:\\d+:\\d+.\\d+,\\d+:\\d+:\\d+.\\d+,\\S+,"; 212 | return subtitleLineString.split(regex)[1]; 213 | } 214 | 215 | /** 216 | * 解析字幕时间 217 | * 218 | * @param timeString 219 | * @param subtitleLineInfo 220 | */ 221 | private boolean parseSubtitleTime(String timeString, SubtitleLineInfo subtitleLineInfo) { 222 | String regex = "\\d+:\\d+:\\d+.\\d+"; 223 | Pattern pattern = Pattern.compile(regex); 224 | Matcher matcher = pattern.matcher(timeString); 225 | if (matcher.find()) { 226 | int startTime = TimeUtil.parseSubtitleTime(matcher.group()); 227 | subtitleLineInfo.setStartTime(startTime); 228 | if (matcher.find()) { 229 | int endTime = TimeUtil.parseSubtitleTime(matcher.group()); 230 | subtitleLineInfo.setEndTime(endTime); 231 | return true; 232 | } 233 | } 234 | return false; 235 | } 236 | 237 | @Override 238 | public boolean isFileSupported(String ext) { 239 | return ext.equalsIgnoreCase(getSupportFileExt()); 240 | } 241 | 242 | @Override 243 | public String getSupportFileExt() { 244 | return "ass"; 245 | } 246 | 247 | /** 248 | * 样式集合 249 | */ 250 | private class Style { 251 | /** 252 | * 样式名称 253 | */ 254 | private String name; 255 | /** 256 | * 字体名称 257 | */ 258 | private String fontname; 259 | /** 260 | * 字体大小 261 | */ 262 | private String fontsize; 263 | /** 264 | * 主体颜色 265 | */ 266 | private String primaryColour; 267 | 268 | /** 269 | * 粗 体( -1=开启,0=关闭) 270 | */ 271 | private String bold; 272 | 273 | /** 274 | * 斜 体( -1=开启,0=关闭) 275 | */ 276 | private String italic; 277 | 278 | /** 279 | * 下划线 ( -1=开启,0=关闭) 280 | */ 281 | private String underline; 282 | 283 | /** 284 | * 删除线( -1=开启,0=关闭) 285 | */ 286 | private String strikeout; 287 | 288 | public String getName() { 289 | return name; 290 | } 291 | 292 | public void setName(String name) { 293 | this.name = name; 294 | } 295 | 296 | public String getFontname() { 297 | return fontname; 298 | } 299 | 300 | public void setFontname(String fontname) { 301 | this.fontname = fontname; 302 | } 303 | 304 | public String getFontsize() { 305 | return fontsize; 306 | } 307 | 308 | public void setFontsize(String fontsize) { 309 | this.fontsize = fontsize; 310 | } 311 | 312 | public String getPrimaryColour() { 313 | return primaryColour; 314 | } 315 | 316 | public void setPrimaryColour(String primaryColour) { 317 | this.primaryColour = primaryColour; 318 | } 319 | 320 | public String getBold() { 321 | return bold; 322 | } 323 | 324 | public void setBold(String bold) { 325 | this.bold = bold; 326 | } 327 | 328 | public String getItalic() { 329 | return italic; 330 | } 331 | 332 | public void setItalic(String italic) { 333 | this.italic = italic; 334 | } 335 | 336 | public String getUnderline() { 337 | return underline; 338 | } 339 | 340 | public void setUnderline(String underline) { 341 | this.underline = underline; 342 | } 343 | 344 | public String getStrikeout() { 345 | return strikeout; 346 | } 347 | 348 | public void setStrikeout(String strikeout) { 349 | this.strikeout = strikeout; 350 | } 351 | 352 | public String getStyleString() { 353 | StringBuilder result = new StringBuilder(); 354 | result.append("{"); 355 | if (!TextUtils.isEmpty(fontname)) { 356 | result.append("\\fn" + fontname); 357 | } 358 | 359 | if (!TextUtils.isEmpty(fontsize)) { 360 | result.append("\\fs" + fontsize); 361 | } 362 | 363 | if (!TextUtils.isEmpty(primaryColour)) { 364 | result.append("\\1c&H" + (primaryColour.replaceAll("&H", "")) + "&"); 365 | } 366 | 367 | if (!TextUtils.isEmpty(bold)) { 368 | result.append("\\b" + Math.abs(Integer.parseInt(bold))); 369 | } 370 | 371 | if (!TextUtils.isEmpty(italic)) { 372 | result.append("\\i" + Math.abs(Integer.parseInt(italic))); 373 | } 374 | 375 | if (!TextUtils.isEmpty(underline)) { 376 | result.append("\\u" + Math.abs(Integer.parseInt(underline))); 377 | } 378 | 379 | if (!TextUtils.isEmpty(strikeout)) { 380 | result.append("\\s" + Math.abs(Integer.parseInt(strikeout))); 381 | } 382 | 383 | result.append("}"); 384 | return result.toString(); 385 | } 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/formats/srt/SrtSubtitleFileReader.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.formats.srt; 2 | 3 | import com.zlm.subtitlelibrary.entity.SubtitleInfo; 4 | import com.zlm.subtitlelibrary.entity.SubtitleLineInfo; 5 | import com.zlm.subtitlelibrary.formats.SubtitleFileReader; 6 | import com.zlm.subtitlelibrary.util.SubtitleUtil; 7 | import com.zlm.subtitlelibrary.util.TimeUtil; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.File; 11 | import java.io.InputStream; 12 | import java.io.InputStreamReader; 13 | import java.nio.charset.Charset; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.regex.Matcher; 17 | import java.util.regex.Pattern; 18 | 19 | /** 20 | * @Description: srt字幕读取器 21 | * @author: zhangliangming 22 | * @date: 2019-01-12 19:35 23 | **/ 24 | public class SrtSubtitleFileReader extends SubtitleFileReader { 25 | public SrtSubtitleFileReader() { 26 | setDefaultCharset(Charset.forName("UTF-16")); 27 | } 28 | 29 | @Override 30 | public SubtitleInfo readInputStream(InputStream in) throws Exception { 31 | if (in != null) { 32 | BufferedReader br = new BufferedReader(new InputStreamReader(in, 33 | getDefaultCharset())); 34 | StringBuilder fileContentSB = new StringBuilder(); 35 | String lineInfo = ""; 36 | while ((lineInfo = br.readLine()) != null) { 37 | fileContentSB.append(lineInfo + "\n"); 38 | } 39 | in.close(); 40 | br.close(); 41 | in = null; 42 | br = null; 43 | return readText(fileContentSB.toString(), null); 44 | } 45 | return null; 46 | } 47 | 48 | @Override 49 | public SubtitleInfo readText(String fileContentString, File saveFile) throws Exception { 50 | saveFile(saveFile, fileContentString); 51 | 52 | SubtitleInfo subtitleInfo = new SubtitleInfo(); 53 | subtitleInfo.setDefaultCharset(getDefaultCharset()); 54 | subtitleInfo.setExt(getSupportFileExt()); 55 | 56 | List subtitleLineInfos = new ArrayList(); 57 | 58 | String[] fileContents = fileContentString.split("\n\n"); 59 | for (int i = 0; i < fileContents.length; i++) { 60 | String subtitleLineString = fileContents[i]; 61 | parseSubtitleInfo(subtitleLineString, subtitleLineInfos); 62 | } 63 | 64 | //设置字幕 65 | if (subtitleLineInfos != null && subtitleLineInfos.size() > 0) { 66 | subtitleInfo.setSubtitleLineInfos(subtitleLineInfos); 67 | } 68 | 69 | return subtitleInfo; 70 | } 71 | 72 | /** 73 | * 解析字幕内容 74 | * 75 | * @param subtitleLineString 字幕行内容 76 | * @param subtitleLineInfos 字幕内容 77 | * @author: zhangliangming 78 | * @date: 2019-01-12 21:15 79 | */ 80 | private void parseSubtitleInfo(String subtitleLineString, List subtitleLineInfos) { 81 | String[] subtitleLines = subtitleLineString.split("\n"); 82 | if (subtitleLines.length >= 3) { 83 | SubtitleLineInfo subtitleLineInfo = new SubtitleLineInfo(); 84 | String timeString = subtitleLines[1]; 85 | boolean flag = parseSubtitleTime(timeString, subtitleLineInfo); 86 | if (!flag) return; 87 | 88 | //加载字幕 89 | String subtitleHtmlString = ""; 90 | String subtitleTextString = ""; 91 | for (int i = 2; i < subtitleLines.length; i++) { 92 | String[] result = SubtitleUtil.parseSubtitleText(subtitleLines[i]); 93 | subtitleTextString += result[0]; 94 | subtitleHtmlString += result[1]; 95 | if (i != subtitleLines.length - 1) { 96 | subtitleTextString += "\n"; 97 | subtitleHtmlString += "
"; 98 | } 99 | } 100 | subtitleLineInfo.setSubtitleText(subtitleTextString); 101 | subtitleLineInfo.setSubtitleHtml(subtitleHtmlString); 102 | subtitleLineInfos.add(subtitleLineInfo); 103 | } 104 | } 105 | 106 | /** 107 | * 解析字幕时间 108 | * 109 | * @param timeString 110 | * @param subtitleLineInfo 111 | */ 112 | private boolean parseSubtitleTime(String timeString, SubtitleLineInfo subtitleLineInfo) { 113 | String regex = "\\d+:\\d+:\\d+,\\d+"; 114 | Pattern pattern = Pattern.compile(regex); 115 | Matcher matcher = pattern.matcher(timeString); 116 | if (matcher.find()) { 117 | int startTime = TimeUtil.parseSubtitleTime(matcher.group()); 118 | subtitleLineInfo.setStartTime(startTime); 119 | if (matcher.find()) { 120 | int endTime = TimeUtil.parseSubtitleTime(matcher.group()); 121 | subtitleLineInfo.setEndTime(endTime); 122 | return true; 123 | } 124 | } 125 | return false; 126 | } 127 | 128 | @Override 129 | public boolean isFileSupported(String ext) { 130 | return ext.equalsIgnoreCase(getSupportFileExt()); 131 | } 132 | 133 | @Override 134 | public String getSupportFileExt() { 135 | return "srt"; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/formats/srt/SrtSubtitleFileWriter.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.formats.srt; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.zlm.subtitlelibrary.entity.SubtitleInfo; 6 | import com.zlm.subtitlelibrary.entity.SubtitleLineInfo; 7 | import com.zlm.subtitlelibrary.formats.SubtitleFileWriter; 8 | import com.zlm.subtitlelibrary.util.SubtitleUtil; 9 | import com.zlm.subtitlelibrary.util.TimeUtil; 10 | 11 | import java.nio.charset.Charset; 12 | import java.util.List; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | /** 17 | * @Description: src字幕保存器 18 | * @author: zhangliangming 19 | * @date: 2019-01-13 16:27 20 | **/ 21 | public class SrtSubtitleFileWriter extends SubtitleFileWriter { 22 | 23 | public SrtSubtitleFileWriter() { 24 | setDefaultCharset(Charset.forName("UTF-16")); 25 | } 26 | 27 | @Override 28 | public boolean writer(SubtitleInfo subtitleInfo, String filePath) throws Exception { 29 | String subtitleContent = getSubtitleContent(subtitleInfo); 30 | return saveFile(subtitleContent, filePath); 31 | } 32 | 33 | @Override 34 | public String getSubtitleContent(SubtitleInfo subtitleInfo) throws Exception { 35 | StringBuilder result = new StringBuilder(); 36 | List subtitleLineInfos = subtitleInfo.getSubtitleLineInfos(); 37 | if (subtitleLineInfos != null && subtitleLineInfos.size() > 0) { 38 | for (int i = 0; i < subtitleLineInfos.size(); i++) { 39 | SubtitleLineInfo subtitleLineInfo = subtitleLineInfos.get(i); 40 | result.append((i + 1) + "\n"); 41 | result.append(TimeUtil.parseHHMMSSFFFString(subtitleLineInfo.getStartTime()) + " --> " + TimeUtil.parseHHMMSSFFFString(subtitleLineInfo.getEndTime()) + "\n"); 42 | String lineText = getSubtitleLineText(subtitleLineInfo.getSubtitleHtml()); 43 | result.append(lineText + "\n\n"); 44 | } 45 | } 46 | return result.toString(); 47 | } 48 | 49 | /** 50 | * 获取字幕行内容 51 | * 52 | * @param subtitleText 53 | * @return 54 | */ 55 | private String getSubtitleLineText(String subtitleText) { 56 | StringBuilder result = new StringBuilder(); 57 | String regex = "(\\)(\\<[bius]\\>)*[^\\<]+(\\)*(\\)"; 58 | int index = 0; 59 | Pattern pattern = Pattern.compile(regex); 60 | Matcher matcher = pattern.matcher(subtitleText); 61 | String[] splitSubtitles = subtitleText.split(regex); 62 | while (matcher.find()) { 63 | if (index < splitSubtitles.length && !TextUtils.isEmpty(splitSubtitles[index])) { 64 | result.append(splitSubtitles[index]); 65 | } 66 | String styleString = matcher.group(); 67 | String subtitleString = parseStyleString(styleString); 68 | result.append(subtitleString); 69 | index++; 70 | } 71 | if (index == 0) { 72 | result.append(subtitleText); 73 | } 74 | 75 | //添加剩余的字幕内容 76 | for (index++; index < splitSubtitles.length; index++) { 77 | result.append(splitSubtitles[index]); 78 | } 79 | 80 | return result.toString().replaceAll("
", "\n"); 81 | } 82 | 83 | /** 84 | * 解析style对应的字符内容 85 | * 86 | * @param styleString 87 | * @return 88 | */ 89 | private String parseStyleString(String styleString) { 90 | StringBuilder result = new StringBuilder(); 91 | String regex = "((\\)(\\<[bius]\\>)*|(\\)*(\\))"; 92 | Pattern pattern = Pattern.compile(regex); 93 | Matcher matcher = pattern.matcher(styleString); 94 | String[] splitString = styleString.split(regex); 95 | if (matcher.find()) { 96 | String style = parseStyle(matcher.group()); 97 | result.append(style); 98 | } 99 | String subtitleText = splitString[1]; 100 | result.append(subtitleText); 101 | return result.toString(); 102 | } 103 | 104 | /** 105 | * 解析sytle 106 | * 107 | * @param styleString 108 | * @return 109 | */ 110 | private String parseStyle(String styleString) { 111 | StringBuilder result = new StringBuilder(); 112 | result.append("{"); 113 | String regex = "\\<[bius]\\>|\\"; 114 | Pattern pattern = Pattern.compile(regex); 115 | Matcher matcher = pattern.matcher(styleString); 116 | while (matcher.find()) { 117 | String style = matcher.group(); 118 | if (style.startsWith("")) { 160 | result.append("\\b1"); 161 | } else if (style.startsWith("")) { 162 | result.append("\\i1"); 163 | } else if (style.startsWith("")) { 164 | result.append("\\u1"); 165 | } else if (style.startsWith("")) { 166 | result.append("\\s1"); 167 | } 168 | } 169 | result.append("}"); 170 | return result.toString(); 171 | } 172 | 173 | @Override 174 | public boolean isFileSupported(String ext) { 175 | return ext.equalsIgnoreCase(getSupportFileExt()); 176 | } 177 | 178 | @Override 179 | public String getSupportFileExt() { 180 | return "srt"; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/util/FileUtil.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.util; 2 | 3 | /** 4 | * @Description: 文件处理类 5 | * @author: zhangliangming 6 | * @date: 2019-01-13 16:19 7 | **/ 8 | public class FileUtil { 9 | 10 | public static String getFileExt(String fileName) { 11 | int pos = fileName.lastIndexOf("."); 12 | if (pos == -1) 13 | return ""; 14 | return fileName.substring(pos + 1).toLowerCase(); 15 | } 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/util/SubtitleUtil.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.util; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.zlm.subtitlelibrary.entity.SubtitleLineInfo; 6 | import com.zlm.subtitlelibrary.formats.SubtitleFileReader; 7 | import com.zlm.subtitlelibrary.formats.SubtitleFileWriter; 8 | import com.zlm.subtitlelibrary.formats.ass.AssSubtitleFileReader; 9 | import com.zlm.subtitlelibrary.formats.srt.SrtSubtitleFileReader; 10 | import com.zlm.subtitlelibrary.formats.srt.SrtSubtitleFileWriter; 11 | 12 | import java.io.File; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.regex.Matcher; 16 | import java.util.regex.Pattern; 17 | 18 | /** 19 | * @Description: 字幕处理工具 20 | * @author: zhangliangming 21 | * @date: 2019-01-12 15:57 22 | **/ 23 | public class SubtitleUtil { 24 | 25 | private static ArrayList readers; 26 | private static ArrayList writers; 27 | 28 | static { 29 | readers = new ArrayList(); 30 | readers.add(new SrtSubtitleFileReader()); 31 | readers.add(new AssSubtitleFileReader()); 32 | // 33 | writers = new ArrayList(); 34 | writers.add(new SrtSubtitleFileWriter()); 35 | } 36 | 37 | /** 38 | * 获取支持的文件格式 39 | * 40 | * @return 41 | */ 42 | public static List getSupportSubtitleExts() { 43 | List lrcExts = new ArrayList(); 44 | for (SubtitleFileReader subtitleFileReader : readers) { 45 | lrcExts.add(subtitleFileReader.getSupportFileExt()); 46 | } 47 | return lrcExts; 48 | } 49 | 50 | /** 51 | * 获取文件读取器 52 | * 53 | * @param file 54 | * @return 55 | */ 56 | public static SubtitleFileReader getSubtitleFileReader(File file) { 57 | return getSubtitleFileReader(file.getName()); 58 | } 59 | 60 | /** 61 | * 获取歌词文件读取器 62 | * 63 | * @param fileName 64 | * @return 65 | */ 66 | public static SubtitleFileReader getSubtitleFileReader(String fileName) { 67 | String ext = FileUtil.getFileExt(fileName); 68 | for (SubtitleFileReader subtitleFileReader : readers) { 69 | if (subtitleFileReader.isFileSupported(ext)) { 70 | return subtitleFileReader; 71 | } 72 | } 73 | return null; 74 | } 75 | 76 | /** 77 | * 获取保存器 78 | * 79 | * @param file 80 | * @return 81 | */ 82 | public static SubtitleFileWriter getSubtitleFileWriter(File file) { 83 | return getSubtitleFileWriter(file.getName()); 84 | } 85 | 86 | /** 87 | * 获取保存器 88 | * 89 | * @param fileName 90 | * @return 91 | */ 92 | public static SubtitleFileWriter getSubtitleFileWriter(String fileName) { 93 | String ext = FileUtil.getFileExt(fileName); 94 | for (SubtitleFileWriter subtitleFileWriter : writers) { 95 | if (subtitleFileWriter.isFileSupported(ext)) { 96 | return subtitleFileWriter; 97 | } 98 | } 99 | return null; 100 | } 101 | 102 | /** 103 | * 解析字幕文本 104 | * 105 | * @param subtitleLine 106 | * @return html格式对应的字幕文本 107 | */ 108 | public static String[] parseSubtitleText(String subtitleLine) { 109 | String[] result = {"", ""}; 110 | String regex = "\\{[^\\{]+\\}"; 111 | //去掉样式 112 | result[0] = subtitleLine.replaceAll(regex, ""); 113 | //加载样式 114 | Pattern tempPattern = Pattern.compile(regex); 115 | Matcher tempMatcher = tempPattern.matcher(subtitleLine); 116 | if (tempMatcher.find()) { 117 | StringBuilder subtitleTextSB = new StringBuilder(); 118 | String[] splitSubtitles = subtitleLine.split(regex, -1); 119 | int index = 0; 120 | 121 | Pattern pattern = Pattern.compile(regex); 122 | Matcher matcher = pattern.matcher(subtitleLine); 123 | //遍历样式字符串 124 | while (matcher.find()) { 125 | if (index == 0 && splitSubtitles.length > 0 && !TextUtils.isEmpty(splitSubtitles[0])) { 126 | subtitleTextSB.append(splitSubtitles[0]); 127 | } 128 | String styleString = matcher.group(); 129 | if (index + 1 >= splitSubtitles.length) { 130 | break; 131 | } 132 | String splitSubtitle = splitSubtitles[index + 1]; 133 | String subtitleText = getSubtitleText(styleString, splitSubtitle); 134 | subtitleTextSB.append(subtitleText); 135 | 136 | index++; 137 | } 138 | 139 | //如果没有样式 140 | if (index == 0 && splitSubtitles.length > 0 && !TextUtils.isEmpty(splitSubtitles[0])) { 141 | subtitleTextSB.append(splitSubtitles[0]); 142 | } 143 | //添加剩余的字幕内容 144 | for (index++; index < splitSubtitles.length; index++) { 145 | if (!TextUtils.isEmpty(splitSubtitles[index])) { 146 | subtitleTextSB.append(splitSubtitles[index]); 147 | } 148 | } 149 | result[1] = subtitleTextSB.toString(); 150 | } else { 151 | result[1] = subtitleLine; 152 | } 153 | return result; 154 | } 155 | 156 | /** 157 | * 获取字幕文本 158 | * 159 | * @param styleString 样式字符串 160 | * @param splitSubtitle 分隔后的字幕文本 161 | * @return 162 | */ 163 | private static String getSubtitleText(String styleString, String splitSubtitle) { 164 | StringBuilder result = new StringBuilder(); 165 | int start = styleString.indexOf("{"); 166 | int end = styleString.lastIndexOf("}"); 167 | styleString = styleString.substring(start + 1, end); 168 | styleString = styleString.replaceAll("\\\\", "\\$"); 169 | if (styleString.contains("$")) { 170 | result.append(" 粗体,i<0/1> 斜体,u<0/1> 下划线,s<0/1> 删除线(0=关闭,1=开启) 184 | 185 | if (style.startsWith("b1")) { 186 | splitSubtitle = "" + splitSubtitle + ""; 187 | } else if (style.startsWith("i1")) { 188 | splitSubtitle = "" + splitSubtitle + ""; 189 | } else if (style.startsWith("u1")) { 190 | splitSubtitle = "" + splitSubtitle + ""; 191 | } else if (style.startsWith("s1")) { 192 | splitSubtitle = "" + splitSubtitle + ""; 193 | } 194 | 195 | } else if (style.startsWith("c&H") || style.startsWith("1c&H")) { 196 | //c&H& 改变主体颜色(同1c) 197 | //1c&H& 改变主体颜色 198 | int endIndex = style.lastIndexOf("&"); 199 | style = style.substring(0, endIndex).trim(); 200 | String color = ""; 201 | if (style.startsWith("c&H")) { 202 | color = convertRgbColor(style.substring("c&H".length()).trim()); 203 | } else { 204 | color = convertRgbColor(style.substring("1c&H".length()).trim()); 205 | } 206 | result.append(" color=\"#" + color + "\""); 207 | 208 | } 209 | } 210 | result.append(">"); 211 | } 212 | //修改成html标签 213 | if (result.length() > 0) { 214 | result.append(splitSubtitle); 215 | result.append("
"); 216 | } else { 217 | result.append(splitSubtitle); 218 | } 219 | return result.toString(); 220 | } 221 | 222 | /** 223 | * 获取rgb颜色字符串 224 | * 版权归作者所有,任何形式转载请联系作者。 225 | * 作者:无条件积极关注(来自豆瓣) 226 | * 来源:https://www.douban.com/note/658520175/ 227 | *

228 | * 颜色格式:&Haabbggrr,均为十六进制,取值0-F。 229 | * 前2位(alpha)为透明度,00=不透明,FF=DEC255=全透明;后6是BGR蓝绿红颜色。 排在最前的00可以忽略不写, 如:{\c&HFF&}={\c&H0000FF&}为纯红色、&HFFFFFF=纯白色、&HC8000000=透明度为200的黑色。 230 | * 231 | * @param abgrColorString 232 | * @return 233 | */ 234 | public static String convertArgbColor(String abgrColorString) { 235 | if (abgrColorString.length() == 8) { 236 | return abgrColorString.substring(6, 8) + abgrColorString.substring(4, 6) + abgrColorString.substring(2, 4); 237 | } 238 | return abgrColorString.substring(4, 6) + abgrColorString.substring(2, 4) + abgrColorString.substring(0, 2); 239 | } 240 | 241 | /** 242 | * 获取rgb颜色字符串 243 | * 244 | * @param bgrColorString 245 | * @return 246 | */ 247 | public static String convertRgbColor(String bgrColorString) { 248 | return convertArgbColor(bgrColorString); 249 | } 250 | 251 | /** 252 | * 获取bgr颜色字符串 253 | * 254 | * @param rgbColorString 255 | * @return 256 | */ 257 | public static String convertBgrColor(String rgbColorString) { 258 | return convertRgbColor(rgbColorString); 259 | } 260 | 261 | /** 262 | * 获取abgr颜色字符串 263 | * 264 | * @param argbColorString 265 | * @return 266 | */ 267 | public static String convertAbgrColor(String argbColorString) { 268 | return convertRgbColor(argbColorString); 269 | } 270 | 271 | /** 272 | * 根据当前播放进度获取当前行字幕内容 273 | * 274 | * @param subtitleLineInfos 275 | * @param curPlayingTime 276 | * @param playOffset 277 | * @return 278 | */ 279 | public static int getLineNumber(List subtitleLineInfos, long curPlayingTime, long playOffset) { 280 | if (subtitleLineInfos != null && subtitleLineInfos.size() > 0) { 281 | //添加歌词增量 282 | long nowPlayingTime = curPlayingTime + playOffset; 283 | for (int i = 0; i < subtitleLineInfos.size(); i++) { 284 | SubtitleLineInfo subtitleLineInfo = subtitleLineInfos.get(i); 285 | int lineStartTime = subtitleLineInfo.getStartTime(); 286 | int lineEndTime = subtitleLineInfo.getEndTime(); 287 | if (nowPlayingTime < lineStartTime) { 288 | return -1; 289 | } else if (nowPlayingTime >= lineStartTime && nowPlayingTime <= lineEndTime) { 290 | return i; 291 | } 292 | } 293 | } 294 | return -1; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/util/TimeUtil.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.util; 2 | 3 | /** 4 | * @Description: 时间处理类 5 | * @author: zhangliangming 6 | * @date: 2019-01-12 21:37 7 | **/ 8 | public class TimeUtil { 9 | /** 10 | * 解析字幕时间 11 | * 12 | * @param timeString 00:00:00,000 13 | * @return 14 | */ 15 | public static int parseSubtitleTime(String timeString) { 16 | timeString = timeString.replace(",", ":"); 17 | timeString = timeString.replace(".", ":"); 18 | String timedata[] = timeString.split(":"); 19 | int second = 1000; 20 | int minute = 60 * second; 21 | int hour = 60 * minute; 22 | int msec = 0; 23 | if (timedata[3].length() == 2) { 24 | msec = Integer.parseInt(timedata[3]) * 10; 25 | } else { 26 | msec = Integer.parseInt(timedata[3]); 27 | } 28 | return Integer.parseInt(timedata[0]) * hour + Integer.parseInt(timedata[1]) * minute 29 | + Integer.parseInt(timedata[2]) * second + msec; 30 | } 31 | 32 | /** 33 | * 毫秒转时间字符串 34 | * 35 | * @param msecTotal 36 | * @return 00:00:00,000 37 | */ 38 | public static String parseHHMMSSFFFString(int msecTotal) { 39 | int msec = msecTotal % 1000; 40 | msecTotal /= 1000; 41 | int minute = msecTotal / 60; 42 | int hour = minute / 60; 43 | int second = msecTotal % 60; 44 | minute %= 60; 45 | return String.format("%02d:%02d:%02d,%03d", hour, minute, second, msec); 46 | } 47 | 48 | /** 49 | * 毫秒转时间字符串 50 | * 51 | * @param msecTotal 52 | * @return 00:00:00.00 53 | */ 54 | public static String parseHHMMSSFFString(int msecTotal) { 55 | int msec = msecTotal % 1000; 56 | msecTotal /= 1000; 57 | int minute = msecTotal / 60; 58 | int hour = minute / 60; 59 | int second = msecTotal % 60; 60 | minute %= 60; 61 | return String.format("%02d:%02d:%02d.%02d", hour, minute, second, msec / 10); 62 | } 63 | 64 | /** 65 | * 毫秒转时间字符串 66 | * 67 | * @param msecTotal 68 | * @return 00:00:00 69 | */ 70 | public static String parseHHMMSSString(int msecTotal) { 71 | msecTotal /= 1000; 72 | int minute = msecTotal / 60; 73 | int hour = minute / 60; 74 | int second = msecTotal % 60; 75 | minute %= 60; 76 | return String.format("%02d:%02d:%02d", hour, minute, second); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/java/com/zlm/subtitlelibrary/widget/SubtitleView.java: -------------------------------------------------------------------------------- 1 | package com.zlm.subtitlelibrary.widget; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.AppCompatTextView; 5 | import android.util.AttributeSet; 6 | 7 | /** 8 | * @Description: 字幕视图 9 | * @author: zhangliangming 10 | * @date: 2019-01-17 22:17 11 | **/ 12 | public class SubtitleView extends AppCompatTextView { 13 | 14 | public SubtitleView(Context context) { 15 | super(context); 16 | init(context); 17 | } 18 | 19 | public SubtitleView(Context context, AttributeSet attrs) { 20 | super(context, attrs); 21 | init(context); 22 | } 23 | 24 | public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) { 25 | super(context, attrs, defStyleAttr); 26 | init(context); 27 | } 28 | 29 | private void init(Context context) { 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /subtitlelibrary/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SubtitleLibrary 3 | 4 | --------------------------------------------------------------------------------