├── .gitignore
├── .idea
├── codeStyles
│ └── Project.xml
├── compiler.xml
├── gradle.xml
├── markdown-navigator.xml
├── markdown-navigator
│ └── profiles_settings.xml
├── misc.xml
└── runConfigurations.xml
├── README.md
├── build.gradle
├── example
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── avery
│ │ └── subtitle
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── avery
│ │ │ └── subtitle
│ │ │ └── example
│ │ │ ├── MainActivity.java
│ │ │ └── SettingsDialog.java
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── bg_button.xml
│ │ ├── bg_button2.xml
│ │ ├── bg_button_color_purple.xml
│ │ ├── bg_button_color_red.xml
│ │ ├── bg_button_color_white.xml
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ └── dialog_settings.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── avery
│ └── subtitle
│ └── ExampleUnitTest.java
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshot
├── one.png
└── two.png
├── settings.gradle
└── subtitle
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
├── androidTest
└── java
│ └── com
│ └── avery
│ └── subtitle
│ └── ExampleInstrumentedTest.java
├── main
├── AndroidManifest.xml
├── java
│ └── com
│ │ └── avery
│ │ └── subtitle
│ │ ├── DefaultSubtitleEngine.java
│ │ ├── SubtitleEngine.java
│ │ ├── SubtitleFinder.java
│ │ ├── SubtitleLoader.java
│ │ ├── UIRenderTask.java
│ │ ├── cache
│ │ └── SubtitleCache.java
│ │ ├── exception
│ │ └── FatalParsingException.java
│ │ ├── format
│ │ ├── FormatASS.java
│ │ ├── FormatSCC.java
│ │ ├── FormatSRT.java
│ │ ├── FormatSTL.java
│ │ ├── FormatTTML.java
│ │ └── TimedTextFileFormat.java
│ │ ├── model
│ │ ├── Region.java
│ │ ├── Style.java
│ │ ├── Subtitle.java
│ │ ├── Time.java
│ │ └── TimedTextObject.java
│ │ ├── runtime
│ │ ├── AppTaskExecutor.java
│ │ ├── DefaultTaskExecutor.java
│ │ └── TaskExecutor.java
│ │ └── widget
│ │ └── SimpleSubtitleView.java
└── res
│ └── values
│ └── strings.xml
└── test
└── java
└── com
└── avery
└── subtitle
└── ExampleUnitTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/markdown-navigator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/.idea/markdown-navigator/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | CSDN blog: https://blog.csdn.net/han_han_1/article/details/86747472
2 |
3 | [  ](https://bintray.com/averyzhong/AndroidRepo/subtitle-for-android/1.0.6/link)
4 |
5 | # Android外挂字幕组件库(Subtitle For Android)
6 |
7 |
8 | ## 概述
9 | Subtitle For Android 是一个Android平台视频播放多字幕支持库,几乎支持所有的Android版本,可以在需要外挂字幕中的项目集成。支持的字幕格式有:.SRT、.SCC、.ASS、.STL、.TTML格式的字幕文件。集成方式简单,可几行代码就可以使你的播放器支持外挂做字幕的支持
10 |
11 | ## 下载
12 |
13 | ```
14 | implementation 'com.avery:subtitle:1.0.6' // 最新版本号请看上面"Download"气泡后面的数字
15 | ```
16 |
17 | >
18 | > 如果Gradle同步出现如下错误:
19 | > Manifest merger failed : uses-sdk:minSdkVersion xx cannot be smaller than version xx declared in library [com.avery:subtitle:x.x.x]
20 | >
21 | >请在`AndroidManifest.xml`中加入` `
22 | >
23 |
24 | ## 怎样使用?
25 | 1. 在播放器布局文件中添加`SimpleSubtitleView`
26 |
27 | ```
28 |
38 |
39 | ```
40 |
41 | 2. 绑定`MediaPlayer`到`SimpleSubtitleView`
42 |
43 | ```
44 | private SimpleSubtitleView mSubtitleView;
45 |
46 | ....省略无关代码.....
47 |
48 | mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
49 | @Override
50 | public void onPrepared(final MediaPlayer mp) {
51 | // 绑定MediaPlayer
52 | mSubtitleView.bindToMediaPlayer(mp);
53 | // 设置字幕
54 | mSubtitleView.setSubtitlePath(SUBTITLE_URL);
55 | }
56 | });
57 | mVideoView.setVideoURI(Uri.parse(VIDEO_URL));
58 |
59 | ....省略无关代码.....
60 |
61 |
62 | @Override
63 | protected void onDestroy() {
64 | mSubtitleView.destroy(); // 记得销毁
65 | super.onDestroy();
66 | }
67 |
68 | ```
69 |
70 | `SimpleSubtitleView`还有其他与`Activity`生命周期相似的方法:`start()`,`pause()`,`resume()`,`stop()`,`reset()` 可以根据具体集成情况在适当的地方进行调用。
71 |
72 | ## 字幕样式设置
73 | `SimpleSubtitleView`继承自`TextView`,所以`TextView`的所有样式设置都适用于`SimpleSubtitleView`,如设置字幕颜色、字幕大小、字幕对其方式等。
74 |
75 | ## 注意!!!
76 | > 1. 最好在`MediaPlayer`初始化完成后才能调用`SimpleSubtitleView.setSubtitlePath()`方法,最好的时机是在MediaPlayer的`onPrepared`回调方法里调用`SimpleSubtitleView.setSubtitlePath()`。
77 | > 2. 最好在`MediaPlayer`销毁之前先销毁`SimpleSubtitleView`,即调用`SimpleSubtitleView.destroy()`,最好的时机是在调用`MediaPlayer.release()`方法前先调用调用`SimpleSubtitleView.destroy()`。
78 |
79 |
80 | ## 自定义字幕显示控件
81 | 如果不想使用提供的`SimpleSubtitleView`控件,你还可以轻松自定义你自己的显示控件,只需通过
82 | `DefaultSubtitleEngine`来辅助就能办到
83 |
84 | ```
85 | ....
86 | private SubtitleEngine mSubtitleEngine = new DefaultSubtitleEngine();
87 |
88 | mSubtitleEngine.setOnSubtitlePreparedListener(new OnSubtitlePreparedListener() {
89 | @Override
90 | public void onSubtitlePrepared(@Nullable final List subtitles) {
91 | // 启动字幕刷新任务
92 | mSubtitleEngine.start();
93 | }
94 | });
95 |
96 | mSubtitleEngine.setOnSubtitleChangeListener(new OnSubtitleChangeListener() {
97 | @Override
98 | public void onSubtitleChanged(@Nullable final Subtitle subtitle) {
99 | // 拿到Subtitle对象来刷新你自定义过的字幕显示控件,注意subtitle可能为空
100 | // 当subtitle为空时,你应该清除自定义控件已显示的字幕显示
101 | .......
102 | }
103 | });
104 | ....
105 | ```
106 |
107 | 自定义的最后一步就是通过`DefaultSubtitleEngine`的生命周期相应方法:`start()`,`pause()`,`resume()`,`stop()`,`reset()`处理好控件的生命周期,以免导致bug。
108 |
109 | ## 快照
110 | 
111 | 
112 |
113 | ## License
114 | ```
115 | Copyright 2019 AveryZhong
116 |
117 | Licensed under the Apache License, Version 2.0 (the "License");
118 | you may not use this file except in compliance with the License.
119 | You may obtain a copy of the License at
120 |
121 | http://www.apache.org/licenses/LICENSE-2.0
122 |
123 | Unless required by applicable law or agreed to in writing, software
124 | distributed under the License is distributed on an "AS IS" BASIS,
125 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
126 | See the License for the specific language governing permissions and
127 | limitations under the License.
128 | ```
129 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | // Sdk and tools
6 | compileSdkVersion = 28
7 | minSdkVersion = 14
8 | targetSdkVersion = 28
9 |
10 | gradleVersion = '3.3.0'
11 | supportLibraryVersion = '20.0.0'
12 | constraintLayoutVersion = '1.1.3'
13 | junitVersion = '4.12'
14 | testRunnerVersion = '1.0.2'
15 | espressoCoreVersion = '3.0.2'
16 | androidMavenGradlePluginVersion = '2.1'
17 | gradleBintrayPluginVersion = '1.7.3'
18 | }
19 | repositories {
20 | google()
21 | jcenter()
22 |
23 | }
24 | dependencies {
25 | classpath "com.android.tools.build:gradle:$gradleVersion"
26 | classpath "com.github.dcendents:android-maven-gradle-plugin:$androidMavenGradlePluginVersion"
27 | classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradleBintrayPluginVersion"
28 |
29 | // NOTE: Do not place your application dependencies here; they belong
30 | // in the individual module build.gradle files
31 | }
32 | }
33 |
34 | allprojects {
35 | repositories {
36 | google()
37 | jcenter()
38 |
39 | }
40 | }
41 |
42 | task clean(type: Delete) {
43 | delete rootProject.buildDir
44 | }
45 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/example/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion rootProject.compileSdkVersion
5 | defaultConfig {
6 | applicationId "com.avery.subtitle.example"
7 | minSdkVersion rootProject.minSdkVersion
8 | targetSdkVersion rootProject.targetSdkVersion
9 | versionCode 1
10 | versionName "1.0"
11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled false
16 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation fileTree(include: ['*.jar'], dir: 'libs')
23 | implementation "com.android.support:appcompat-v7:28.0.0"
24 | implementation "com.android.support.constraint:constraint-layout:$constraintLayoutVersion"
25 | testImplementation "junit:junit:$junitVersion"
26 | androidTestImplementation "com.android.support.test:runner:$testRunnerVersion"
27 | androidTestImplementation "com.android.support.test.espresso:espresso-core:$espressoCoreVersion"
28 | implementation project(':subtitle')
29 | }
30 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/src/androidTest/java/com/avery/subtitle/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.avery.subtitle;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.avery.subtitle", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/example/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/example/src/main/java/com/avery/subtitle/example/MainActivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) of Avery
3 | *
4 | * _ooOoo_
5 | * o8888888o
6 | * 88" . "88
7 | * (| -_- |)
8 | * O\ = /O
9 | * ____/`- -'\____
10 | * .' \\| |// `.
11 | * / \\||| : |||// \
12 | * / _||||| -:- |||||- \
13 | * | | \\\ - /// | |
14 | * | \_| ''\- -/'' | |
15 | * \ .-\__ `-` ___/-. /
16 | * ___`. .' /- -.- -\ `. . __
17 | * ."" '< `.___\_<|>_/___.' >'"".
18 | * | | : `- \`.;`\ _ /`;.`/ - ` : | |
19 | * \ \ `-. \_ __\ /__ _/ .-` / /
20 | * ======`-.____`-.___\_____/___.-`____.-'======
21 | * `=- -='
22 | * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | * Buddha bless, there will never be bug!!!
24 | */
25 |
26 | package com.avery.subtitle.example;
27 |
28 | import android.content.Context;
29 | import android.graphics.Color;
30 | import android.media.MediaPlayer;
31 | import android.net.Uri;
32 | import android.os.Bundle;
33 | import android.support.v7.app.AppCompatActivity;
34 | import android.view.Gravity;
35 | import android.view.View;
36 | import android.widget.Button;
37 | import android.widget.RelativeLayout;
38 | import android.widget.TextView;
39 | import android.widget.Toast;
40 | import android.widget.VideoView;
41 |
42 | import com.avery.subtitle.widget.SimpleSubtitleView;
43 |
44 |
45 | public class MainActivity extends AppCompatActivity implements View.OnClickListener {
46 |
47 | private static final String VIDEO_URL = "http://172.16.201.228/movie/Ultra.Pulpe.2018.B.Mandico.DVDRip.Cinetik.mkv";
48 | private static final String SUBTITLE_URL = "http://172.16.201.228/movie/subtitles/Ultra.Pulpe.2018.B.Mandico.DVDRip.Cinetik.chs.srt";
49 |
50 | private Context mContext;
51 | private VideoView mVideoView;
52 | private SimpleSubtitleView mSubtitleView;
53 |
54 | private Button mBtnPlayPause;
55 | private Button mBtnSettings;
56 | private Button mBtnForward;
57 | private Button mBtnRewind;
58 | private TextView mTvTips;
59 | private SettingsDialog mSettingsDialog;
60 |
61 | @Override
62 | protected void onCreate(Bundle savedInstanceState) {
63 | super.onCreate(savedInstanceState);
64 | setContentView(R.layout.activity_main);
65 | mContext = this;
66 | initViews();
67 | setupEventListeners();
68 | initPlayer();
69 | }
70 |
71 | private void initViews() {
72 | mVideoView = findViewById(R.id.video_view);
73 | mSubtitleView = findViewById(R.id.subtitle_view);
74 | mTvTips = findViewById(R.id.tv_tips);
75 | mBtnPlayPause = findViewById(R.id.btn_play_pause);
76 | mBtnSettings = findViewById(R.id.btn_settings);
77 | mBtnForward = findViewById(R.id.btn_forward);
78 | mBtnRewind = findViewById(R.id.btn_rewind);
79 | mBtnPlayPause.requestFocus();
80 | }
81 |
82 | private void setupEventListeners() {
83 | mBtnPlayPause.setOnClickListener(this);
84 | mBtnSettings.setOnClickListener(this);
85 | mBtnForward.setOnClickListener(this);
86 | mBtnRewind.setOnClickListener(this);
87 |
88 | }
89 |
90 | private void initPlayer() {
91 | mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
92 | @Override
93 | public void onPrepared(final MediaPlayer mp) {
94 | // 绑定MediaPlayer
95 | mSubtitleView.bindToMediaPlayer(mp);
96 | // 设置字幕
97 | mSubtitleView.setSubtitlePath(SUBTITLE_URL);
98 | }
99 | });
100 | mVideoView.setVideoURI(Uri.parse(VIDEO_URL));
101 | }
102 |
103 | private void play() {
104 | mVideoView.start();
105 | mBtnPlayPause.setText("暂停");
106 | mTvTips.setVisibility(View.GONE);
107 | }
108 |
109 | private void pause() {
110 | mVideoView.pause();
111 | mBtnPlayPause.setText("播放");
112 | }
113 |
114 | @Override
115 | protected void onDestroy() {
116 | mSubtitleView.destroy();
117 | super.onDestroy();
118 | }
119 |
120 | @Override
121 | public void onClick(final View v) {
122 | switch (v.getId()) {
123 | case R.id.btn_play_pause:
124 | if (mVideoView.isPlaying()) {
125 | pause();
126 | } else {
127 | play();
128 | }
129 | break;
130 | case R.id.btn_settings:
131 | showSettingsDialog();
132 | break;
133 | case R.id.btn_forward:
134 | forward();
135 | break;
136 | case R.id.btn_rewind:
137 | rewind();
138 | break;
139 | }
140 | }
141 |
142 |
143 | private void forward() {
144 | int position = mVideoView.getCurrentPosition() + 60 * 1000;
145 | if (position > mVideoView.getDuration()) {
146 | position = mVideoView.getDuration();
147 | Toast.makeText(mContext, "快进到头了", Toast.LENGTH_SHORT).show();
148 | }
149 | mVideoView.seekTo(position);
150 | }
151 |
152 | private void rewind() {
153 | int position = mVideoView.getCurrentPosition() - 60 * 1000;
154 | if (position < 0) {
155 | position = 0;
156 | Toast.makeText(mContext, "快退到头了", Toast.LENGTH_SHORT).show();
157 | }
158 | mVideoView.seekTo(position);
159 | }
160 |
161 | private void showSettingsDialog() {
162 | if (mSettingsDialog == null) {
163 | mSettingsDialog = new SettingsDialog();
164 | mSettingsDialog.setOnSettingListener(new SettingsDialog.OnSettingListener() {
165 | @Override
166 | public void onSubtitleColorChange(final String color) {
167 | mSubtitleView.setTextColor(Color.parseColor(color));
168 | }
169 |
170 | @Override
171 | public void onSubtitleFontSizeChange(final int fontSize) {
172 | mSubtitleView.setTextSize(fontSize);
173 | }
174 |
175 | @Override
176 | public void onSubtitlePositionChange(final Position position) {
177 | RelativeLayout.LayoutParams lp
178 | = (RelativeLayout.LayoutParams) mSubtitleView.getLayoutParams();
179 | switch (position) {
180 | case TOP:
181 | mSubtitleView
182 | .setGravity(Gravity.CENTER_HORIZONTAL | Gravity.TOP);
183 | break;
184 | case CENTER:
185 | mSubtitleView
186 | .setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL);
187 | break;
188 | case BOTTOM:
189 | mSubtitleView
190 | .setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
191 | break;
192 | }
193 | mSubtitleView.setLayoutParams(lp);
194 | }
195 | });
196 | }
197 | mSettingsDialog.show(getSupportFragmentManager(), "SettingsDialog");
198 | }
199 |
200 |
201 | }
202 |
--------------------------------------------------------------------------------
/example/src/main/java/com/avery/subtitle/example/SettingsDialog.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) of Avery
3 | *
4 | * _ooOoo_
5 | * o8888888o
6 | * 88" . "88
7 | * (| -_- |)
8 | * O\ = /O
9 | * ____/`- -'\____
10 | * .' \\| |// `.
11 | * / \\||| : |||// \
12 | * / _||||| -:- |||||- \
13 | * | | \\\ - /// | |
14 | * | \_| ''\- -/'' | |
15 | * \ .-\__ `-` ___/-. /
16 | * ___`. .' /- -.- -\ `. . __
17 | * ."" '< `.___\_<|>_/___.' >'"".
18 | * | | : `- \`.;`\ _ /`;.`/ - ` : | |
19 | * \ \ `-. \_ __\ /__ _/ .-` / /
20 | * ======`-.____`-.___\_____/___.-`____.-'======
21 | * `=- -='
22 | * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | * Buddha bless, there will never be bug!!!
24 | */
25 |
26 | package com.avery.subtitle.example;
27 |
28 | import android.os.Bundle;
29 | import android.support.annotation.NonNull;
30 | import android.support.annotation.Nullable;
31 | import android.support.v4.app.DialogFragment;
32 | import android.view.LayoutInflater;
33 | import android.view.View;
34 | import android.view.ViewGroup;
35 | import android.widget.Button;
36 |
37 | /**
38 | * @author AveryZhong.
39 | */
40 |
41 | public class SettingsDialog extends DialogFragment implements View.OnClickListener {
42 |
43 | @Override
44 | public void onCreate(@Nullable final Bundle savedInstanceState) {
45 | super.onCreate(savedInstanceState);
46 | setStyle(STYLE_NO_FRAME, android.R.style.Theme_Translucent_NoTitleBar_Fullscreen);
47 | }
48 |
49 | @Nullable
50 | @Override
51 | public View onCreateView(@NonNull final LayoutInflater inflater,
52 | @Nullable final ViewGroup container,
53 | @Nullable final Bundle savedInstanceState) {
54 | return inflater.inflate(R.layout.dialog_settings, container, false);
55 | }
56 |
57 | @Override
58 | public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
59 | super.onViewCreated(view, savedInstanceState);
60 | final Button btnColorWhite = view.findViewById(R.id.btn_white);
61 | final Button btnColorRed = view.findViewById(R.id.btn_red);
62 | final Button btnColorPurple = view.findViewById(R.id.btn_purple);
63 |
64 | final Button btnFontSizeSmall = view.findViewById(R.id.btn_small);
65 | final Button btnFontSizeMiddle = view.findViewById(R.id.btn_middle);
66 | final Button btnFontSizeBig = view.findViewById(R.id.btn_big);
67 |
68 | final Button btnPositionTop = view.findViewById(R.id.btn_top);
69 | final Button btnPositionCenter = view.findViewById(R.id.btn_center);
70 | final Button btnPositionBottom = view.findViewById(R.id.btn_bottom);
71 |
72 |
73 | btnColorWhite.setOnClickListener(this);
74 | btnColorRed.setOnClickListener(this);
75 | btnColorPurple.setOnClickListener(this);
76 |
77 | btnFontSizeSmall.setOnClickListener(this);
78 | btnFontSizeMiddle.setOnClickListener(this);
79 | btnFontSizeBig.setOnClickListener(this);
80 |
81 | btnPositionTop.setOnClickListener(this);
82 | btnPositionCenter.setOnClickListener(this);
83 | btnPositionBottom.setOnClickListener(this);
84 |
85 | }
86 |
87 | @Override
88 | public void onClick(final View v) {
89 | if (mOnSettingListener == null) {
90 | return;
91 | }
92 | switch (v.getId()) {
93 | case R.id.btn_white:
94 | mOnSettingListener.onSubtitleColorChange("#ffffff");
95 | break;
96 | case R.id.btn_red:
97 | mOnSettingListener.onSubtitleColorChange("#ff0000");
98 | break;
99 | case R.id.btn_purple:
100 | mOnSettingListener.onSubtitleColorChange("#7F0E92");
101 | break;
102 |
103 | case R.id.btn_small:
104 | mOnSettingListener.onSubtitleFontSizeChange(12);
105 | break;
106 | case R.id.btn_middle:
107 | mOnSettingListener.onSubtitleFontSizeChange(16);
108 | break;
109 | case R.id.btn_big:
110 | mOnSettingListener.onSubtitleFontSizeChange(22);
111 | break;
112 |
113 | case R.id.btn_top:
114 | mOnSettingListener
115 | .onSubtitlePositionChange(OnSettingListener.Position.TOP);
116 | break;
117 | case R.id.btn_center:
118 | mOnSettingListener
119 | .onSubtitlePositionChange(OnSettingListener.Position.CENTER);
120 | break;
121 | case R.id.btn_bottom:
122 | mOnSettingListener
123 | .onSubtitlePositionChange(OnSettingListener.Position.BOTTOM);
124 | break;
125 | }
126 | }
127 |
128 | private OnSettingListener mOnSettingListener;
129 |
130 | public void setOnSettingListener(final OnSettingListener onSettingListener) {
131 | mOnSettingListener = onSettingListener;
132 | }
133 |
134 | public interface OnSettingListener {
135 | void onSubtitleColorChange(String color);
136 |
137 | void onSubtitleFontSizeChange(int fontSize);
138 |
139 | void onSubtitlePositionChange(Position position);
140 |
141 | enum Position {
142 | TOP, CENTER, BOTTOM
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/bg_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 |
42 | -
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/bg_button2.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/bg_button_color_purple.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | -
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | -
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/bg_button_color_red.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 |
36 | -
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | -
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/bg_button_color_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | -
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | -
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
33 |
36 |
41 |
46 |
51 |
56 |
61 |
66 |
71 |
76 |
81 |
86 |
91 |
96 |
101 |
106 |
111 |
116 |
121 |
126 |
131 |
136 |
141 |
146 |
151 |
156 |
161 |
166 |
171 |
176 |
181 |
186 |
191 |
196 |
197 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
34 |
35 |
41 |
42 |
52 |
53 |
62 |
63 |
70 |
71 |
81 |
82 |
92 |
93 |
103 |
104 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/dialog_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
31 |
32 |
40 |
41 |
47 |
48 |
55 |
56 |
60 |
61 |
67 |
68 |
74 |
75 |
81 |
82 |
83 |
84 |
85 |
86 |
92 |
93 |
99 |
100 |
104 |
105 |
115 |
116 |
126 |
127 |
137 |
138 |
139 |
140 |
141 |
142 |
148 |
149 |
156 |
157 |
161 |
162 |
172 |
173 |
183 |
184 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/example/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/example/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/example/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/example/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/example/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/example/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/example/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SubtitleForAndroid
3 |
4 |
--------------------------------------------------------------------------------
/example/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/src/test/java/com/avery/subtitle/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.avery.subtitle;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/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 |
15 |
16 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jan 31 10:20:58 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/screenshot/one.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/screenshot/one.png
--------------------------------------------------------------------------------
/screenshot/two.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/averyzhong/SubtitleForAndroid/ff47571dafadcbd03faa700d272f66e24cace881/screenshot/two.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':example', ':subtitle'
2 |
--------------------------------------------------------------------------------
/subtitle/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/subtitle/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply from: 'maven-deployer.gradle'
3 |
4 | android {
5 | compileSdkVersion rootProject.compileSdkVersion
6 |
7 | defaultConfig {
8 | minSdkVersion rootProject.minSdkVersion
9 | targetSdkVersion rootProject.targetSdkVersion
10 | versionCode 6
11 | versionName "1.0.6"
12 |
13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
14 |
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 |
24 | }
25 |
26 | dependencies {
27 | implementation fileTree(dir: 'libs', include: ['*.jar'])
28 |
29 | implementation "com.android.support:appcompat-v7:$supportLibraryVersion"
30 | testImplementation "junit:junit:$junitVersion"
31 | androidTestImplementation "com.android.support.test:runner:$testRunnerVersion"
32 | androidTestImplementation "com.android.support.test.espresso:espresso-core:$espressoCoreVersion"
33 | }
34 |
35 | apply from: 'jcenter-deployer.gradle'
36 |
--------------------------------------------------------------------------------
/subtitle/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 |
--------------------------------------------------------------------------------
/subtitle/src/androidTest/java/com/avery/subtitle/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.avery.subtitle;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.avery.subtitle.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/subtitle/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/subtitle/src/main/java/com/avery/subtitle/DefaultSubtitleEngine.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) of Avery
3 | *
4 | * _ooOoo_
5 | * o8888888o
6 | * 88" . "88
7 | * (| -_- |)
8 | * O\ = /O
9 | * ____/`- -'\____
10 | * .' \\| |// `.
11 | * / \\||| : |||// \
12 | * / _||||| -:- |||||- \
13 | * | | \\\ - /// | |
14 | * | \_| ''\- -/'' | |
15 | * \ .-\__ `-` ___/-. /
16 | * ___`. .' /- -.- -\ `. . __
17 | * ."" '< `.___\_<|>_/___.' >'"".
18 | * | | : `- \`.;`\ _ /`;.`/ - ` : | |
19 | * \ \ `-. \_ __\ /__ _/ .-` / /
20 | * ======`-.____`-.___\_____/___.-`____.-'======
21 | * `=- -='
22 | * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | * Buddha bless, there will never be bug!!!
24 | */
25 |
26 | package com.avery.subtitle;
27 |
28 | import android.media.MediaPlayer;
29 | import android.os.Handler;
30 | import android.os.HandlerThread;
31 | import android.os.Message;
32 | import android.support.annotation.Nullable;
33 | import android.text.TextUtils;
34 | import android.util.Log;
35 |
36 | import com.avery.subtitle.cache.SubtitleCache;
37 | import com.avery.subtitle.model.Subtitle;
38 | import com.avery.subtitle.model.TimedTextObject;
39 |
40 | import java.util.ArrayList;
41 | import java.util.List;
42 | import java.util.TreeMap;
43 |
44 | /**
45 | * @author AveryZhong.
46 | */
47 |
48 | public class DefaultSubtitleEngine implements SubtitleEngine {
49 | private static final String TAG = DefaultSubtitleEngine.class.getSimpleName();
50 | private static final int MSG_REFRESH = 0x888;
51 | private static final int REFRESH_INTERVAL = 100;
52 |
53 | @Nullable
54 | private HandlerThread mHandlerThread;
55 | @Nullable
56 | private Handler mWorkHandler;
57 | @Nullable
58 | private List mSubtitles;
59 | private UIRenderTask mUIRenderTask;
60 | private MediaPlayer mMediaPlayer;
61 | private SubtitleCache mCache;
62 | private OnSubtitlePreparedListener mOnSubtitlePreparedListener;
63 | private OnSubtitleChangeListener mOnSubtitleChangeListener;
64 |
65 | public DefaultSubtitleEngine() {
66 | mCache = new SubtitleCache();
67 |
68 | }
69 |
70 | @Override
71 | public void bindToMediaPlayer(final MediaPlayer mediaPlayer) {
72 | mMediaPlayer = mediaPlayer;
73 | }
74 |
75 | @Override
76 | public void setSubtitlePath(final String path) {
77 | doOnSubtitlePathSet();
78 | if (TextUtils.isEmpty(path)) {
79 | Log.w(TAG, "loadSubtitleFromRemote: path is null.");
80 | return;
81 | }
82 | mSubtitles = mCache.get(path);
83 | if (mSubtitles != null && !mSubtitles.isEmpty()) {
84 | Log.d(TAG, "from cache.");
85 | notifyPrepared();
86 | return;
87 | }
88 | SubtitleLoader.loadSubtitle(path, new SubtitleLoader.Callback() {
89 | @Override
90 | public void onSuccess(final TimedTextObject timedTextObject) {
91 | if (timedTextObject == null) {
92 | Log.d(TAG, "onSuccess: timedTextObject is null.");
93 | return;
94 | }
95 | final TreeMap captions = timedTextObject.captions;
96 | if (captions == null) {
97 | Log.d(TAG, "onSuccess: captions is null.");
98 | return;
99 | }
100 | mSubtitles = new ArrayList<>(captions.values());
101 | notifyPrepared();
102 | mCache.put(path, new ArrayList<>(captions.values()));
103 | }
104 |
105 | @Override
106 | public void onError(final Exception exception) {
107 | Log.e(TAG, "onError: " + exception.getMessage());
108 | }
109 | });
110 | }
111 |
112 | private void doOnSubtitlePathSet() {
113 | reset();
114 | createWorkThread();
115 |
116 | }
117 |
118 | @Override
119 | public void reset() {
120 | stopWorkThread();
121 | mSubtitles = null;
122 | mUIRenderTask = null;
123 |
124 | }
125 |
126 | @Override
127 | public void start() {
128 | Log.d(TAG, "start: ");
129 | if (mMediaPlayer == null) {
130 | Log.w(TAG, "MediaPlayer is not bind, You must bind MediaPlayer to "
131 | + SubtitleEngine.class.getSimpleName()
132 | + " before start() method be called,"
133 | + " you can do this by call " +
134 | "bindToMediaPlayer(MediaPlayer mediaPlayer) method.");
135 | return;
136 | }
137 | if (mWorkHandler != null) {
138 | mWorkHandler.removeMessages(MSG_REFRESH);
139 | mWorkHandler.sendEmptyMessageDelayed(MSG_REFRESH, REFRESH_INTERVAL);
140 | }
141 |
142 | }
143 |
144 | @Override
145 | public void pause() {
146 | if (mWorkHandler != null) {
147 | mWorkHandler.removeMessages(MSG_REFRESH);
148 | }
149 | }
150 |
151 | @Override
152 | public void resume() {
153 | start();
154 | }
155 |
156 | @Override
157 | public void stop() {
158 | if (mWorkHandler != null) {
159 | mWorkHandler.removeMessages(MSG_REFRESH);
160 | }
161 | }
162 |
163 | @Override
164 | public void destroy() {
165 | Log.d(TAG, "destroy: ");
166 | stopWorkThread();
167 | mSubtitles = null;
168 | mUIRenderTask = null;
169 |
170 | }
171 |
172 | private void createWorkThread() {
173 | mHandlerThread = new HandlerThread("SubtitleFindThread");
174 | mHandlerThread.start();
175 | mWorkHandler = new Handler(mHandlerThread.getLooper(), new Handler.Callback() {
176 | @Override
177 | public boolean handleMessage(final Message msg) {
178 | try {
179 | long delay = REFRESH_INTERVAL;
180 | if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
181 | long position = mMediaPlayer.getCurrentPosition();
182 | Subtitle subtitle = SubtitleFinder.find(position, mSubtitles);
183 | notifyRefreshUI(subtitle);
184 | if (subtitle != null) {
185 | delay = subtitle.end.mseconds - position;
186 | }
187 |
188 | }
189 | if (mWorkHandler != null) {
190 | mWorkHandler.sendEmptyMessageDelayed(MSG_REFRESH, delay);
191 | }
192 | } catch (Exception e) {
193 | // ignored
194 | }
195 | return true;
196 | }
197 | });
198 | }
199 |
200 | private void stopWorkThread() {
201 | if (mHandlerThread != null) {
202 | mHandlerThread.quit();
203 | mHandlerThread = null;
204 | }
205 | if (mWorkHandler != null) {
206 | mWorkHandler.removeCallbacksAndMessages(null);
207 | mWorkHandler = null;
208 | }
209 | }
210 |
211 | private void notifyRefreshUI(final Subtitle subtitle) {
212 | if (mUIRenderTask == null) {
213 | mUIRenderTask = new UIRenderTask(mOnSubtitleChangeListener);
214 | }
215 | mUIRenderTask.execute(subtitle);
216 | }
217 |
218 | private void notifyPrepared() {
219 | if (mOnSubtitlePreparedListener != null) {
220 | mOnSubtitlePreparedListener.onSubtitlePrepared(mSubtitles);
221 | }
222 | }
223 |
224 | @Override
225 | public void setOnSubtitlePreparedListener(final OnSubtitlePreparedListener listener) {
226 | mOnSubtitlePreparedListener = listener;
227 | }
228 |
229 | @Override
230 | public void setOnSubtitleChangeListener(final OnSubtitleChangeListener listener) {
231 | mOnSubtitleChangeListener = listener;
232 | }
233 |
234 | }
235 |
--------------------------------------------------------------------------------
/subtitle/src/main/java/com/avery/subtitle/SubtitleEngine.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) of Avery
3 | *
4 | * _ooOoo_
5 | * o8888888o
6 | * 88" . "88
7 | * (| -_- |)
8 | * O\ = /O
9 | * ____/`- -'\____
10 | * .' \\| |// `.
11 | * / \\||| : |||// \
12 | * / _||||| -:- |||||- \
13 | * | | \\\ - /// | |
14 | * | \_| ''\- -/'' | |
15 | * \ .-\__ `-` ___/-. /
16 | * ___`. .' /- -.- -\ `. . __
17 | * ."" '< `.___\_<|>_/___.' >'"".
18 | * | | : `- \`.;`\ _ /`;.`/ - ` : | |
19 | * \ \ `-. \_ __\ /__ _/ .-` / /
20 | * ======`-.____`-.___\_____/___.-`____.-'======
21 | * `=- -='
22 | * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | * Buddha bless, there will never be bug!!!
24 | */
25 |
26 | package com.avery.subtitle;
27 |
28 | import android.media.MediaPlayer;
29 | import android.support.annotation.Nullable;
30 |
31 | import com.avery.subtitle.model.Subtitle;
32 |
33 | import java.util.List;
34 |
35 | /**
36 | * @author AveryZhong.
37 | */
38 |
39 | public interface SubtitleEngine {
40 |
41 | /**
42 | * 设置字幕路径,加载字幕
43 | *
44 | * @param path 字幕路径(本地路径或者是远程路径)
45 | */
46 | void setSubtitlePath(String path);
47 |
48 | /**
49 | * 开启字幕刷新任务
50 | */
51 | void start();
52 |
53 | /**
54 | * 暂停
55 | */
56 | void pause();
57 |
58 | /**
59 | * 恢复
60 | */
61 | void resume();
62 |
63 | /**
64 | * 停止字幕刷新任务
65 | */
66 | void stop();
67 |
68 | /**
69 | * 重置
70 | */
71 | void reset();
72 |
73 | /**
74 | * 销毁字幕
75 | */
76 | void destroy();
77 |
78 | /**
79 | * 绑定MediaPlayer
80 | *
81 | * @param mediaPlayer mediaPlayer
82 | */
83 | void bindToMediaPlayer(MediaPlayer mediaPlayer);
84 |
85 | /**
86 | * 设置字幕准备完成监接口
87 | *
88 | * @param listener OnSubtitlePreparedListener
89 | */
90 | void setOnSubtitlePreparedListener(OnSubtitlePreparedListener listener);
91 |
92 | /**
93 | * 设置字幕改变监听接口
94 | *
95 | * @param listener OnSubtitleChangeListener
96 | */
97 | void setOnSubtitleChangeListener(OnSubtitleChangeListener listener);
98 |
99 | /**
100 | * 幕准备完成监接口
101 | */
102 | interface OnSubtitlePreparedListener {
103 | void onSubtitlePrepared(@Nullable List subtitles);
104 | }
105 |
106 | /**
107 | * 字幕改变监听接口
108 | */
109 | interface OnSubtitleChangeListener {
110 | void onSubtitleChanged(@Nullable Subtitle subtitle);
111 | }
112 |
113 | }
--------------------------------------------------------------------------------
/subtitle/src/main/java/com/avery/subtitle/SubtitleFinder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) of Avery
3 | *
4 | * _ooOoo_
5 | * o8888888o
6 | * 88" . "88
7 | * (| -_- |)
8 | * O\ = /O
9 | * ____/`- -'\____
10 | * .' \\| |// `.
11 | * / \\||| : |||// \
12 | * / _||||| -:- |||||- \
13 | * | | \\\ - /// | |
14 | * | \_| ''\- -/'' | |
15 | * \ .-\__ `-` ___/-. /
16 | * ___`. .' /- -.- -\ `. . __
17 | * ."" '< `.___\_<|>_/___.' >'"".
18 | * | | : `- \`.;`\ _ /`;.`/ - ` : | |
19 | * \ \ `-. \_ __\ /__ _/ .-` / /
20 | * ======`-.____`-.___\_____/___.-`____.-'======
21 | * `=- -='
22 | * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | * Buddha bless, there will never be bug!!!
24 | */
25 |
26 | package com.avery.subtitle;
27 |
28 | import android.support.annotation.Nullable;
29 |
30 | import com.avery.subtitle.model.Subtitle;
31 |
32 | import java.util.List;
33 |
34 | /**
35 | * @author AveryZhong.
36 | */
37 |
38 | public class SubtitleFinder {
39 | private SubtitleFinder() {
40 | throw new AssertionError("No instance for you");
41 | }
42 |
43 | @Nullable
44 | public static Subtitle find(long position, List subtitles) {
45 | if (subtitles == null || subtitles.isEmpty()) {
46 | return null;
47 | }
48 | int start = 0;
49 | int end = subtitles.size() - 1;
50 | while (start <= end) {
51 | int middle = (start + end) / 2;
52 | Subtitle middleSubtitle = subtitles.get(middle);
53 | if (position < middleSubtitle.start.mseconds) {
54 | if (position > middleSubtitle.end.mseconds) {
55 | return middleSubtitle;
56 | }
57 | end = middle - 1;
58 | } else if (position > middleSubtitle.end.mseconds) {
59 | if (position < middleSubtitle.start.mseconds) {
60 | return middleSubtitle;
61 | }
62 | start = middle + 1;
63 | } else if (position >= middleSubtitle.start.mseconds
64 | && position <= middleSubtitle.end.mseconds) {
65 | return middleSubtitle;
66 | }
67 | }
68 | return null;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/subtitle/src/main/java/com/avery/subtitle/SubtitleLoader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) of Avery
3 | *
4 | * _ooOoo_
5 | * o8888888o
6 | * 88" . "88
7 | * (| -_- |)
8 | * O\ = /O
9 | * ____/`- -'\____
10 | * .' \\| |// `.
11 | * / \\||| : |||// \
12 | * / _||||| -:- |||||- \
13 | * | | \\\ - /// | |
14 | * | \_| ''\- -/'' | |
15 | * \ .-\__ `-` ___/-. /
16 | * ___`. .' /- -.- -\ `. . __
17 | * ."" '< `.___\_<|>_/___.' >'"".
18 | * | | : `- \`.;`\ _ /`;.`/ - ` : | |
19 | * \ \ `-. \_ __\ /__ _/ .-` / /
20 | * ======`-.____`-.___\_____/___.-`____.-'======
21 | * `=- -='
22 | * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | * Buddha bless, there will never be bug!!!
24 | */
25 |
26 | package com.avery.subtitle;
27 |
28 | import android.text.TextUtils;
29 | import android.util.Log;
30 |
31 | import com.avery.subtitle.exception.FatalParsingException;
32 | import com.avery.subtitle.format.FormatASS;
33 | import com.avery.subtitle.format.FormatSRT;
34 | import com.avery.subtitle.format.FormatSTL;
35 | import com.avery.subtitle.model.TimedTextObject;
36 | import com.avery.subtitle.runtime.AppTaskExecutor;
37 |
38 | import java.io.File;
39 | import java.io.FileInputStream;
40 | import java.io.IOException;
41 | import java.io.InputStream;
42 | import java.net.URL;
43 |
44 | /**
45 | * @author AveryZhong.
46 | */
47 |
48 | public class SubtitleLoader {
49 | private static final String TAG = SubtitleLoader.class.getSimpleName();
50 |
51 | private SubtitleLoader() {
52 | throw new AssertionError("No instance for you.");
53 | }
54 |
55 | public static void loadSubtitle(final String path, final Callback callback) {
56 | if (TextUtils.isEmpty(path)) {
57 | return;
58 | }
59 | if (path.startsWith("http://")
60 | || path.startsWith("https://")) {
61 | loadFromRemoteAsync(path, callback);
62 | } else {
63 | loadFromLocalAsync(path, callback);
64 | }
65 | }
66 |
67 | private static void loadFromRemoteAsync(final String remoteSubtitlePath,
68 | final Callback callback) {
69 | AppTaskExecutor.deskIO().execute(new Runnable() {
70 | @Override
71 | public void run() {
72 | try {
73 | final TimedTextObject timedTextObject = loadFromRemote(remoteSubtitlePath);
74 | if (callback != null) {
75 | AppTaskExecutor.mainThread().execute(new Runnable() {
76 | @Override
77 | public void run() {
78 | callback.onSuccess(timedTextObject);
79 | }
80 | });
81 | }
82 |
83 | } catch (final Exception e) {
84 | e.printStackTrace();
85 | if (callback != null) {
86 | AppTaskExecutor.mainThread().execute(new Runnable() {
87 | @Override
88 | public void run() {
89 | callback.onError(e);
90 | }
91 | });
92 | }
93 |
94 | }
95 | }
96 | });
97 | }
98 |
99 | private static void loadFromLocalAsync(final String localSubtitlePath,
100 | final Callback callback) {
101 | AppTaskExecutor.deskIO().execute(new Runnable() {
102 | @Override
103 | public void run() {
104 | try {
105 | final TimedTextObject timedTextObject = loadFromLocal(localSubtitlePath);
106 | if (callback != null) {
107 | AppTaskExecutor.mainThread().execute(new Runnable() {
108 | @Override
109 | public void run() {
110 | callback.onSuccess(timedTextObject);
111 | }
112 | });
113 | }
114 |
115 | } catch (final Exception e) {
116 | e.printStackTrace();
117 | if (callback != null) {
118 | AppTaskExecutor.mainThread().execute(new Runnable() {
119 | @Override
120 | public void run() {
121 | callback.onError(e);
122 | }
123 | });
124 | }
125 |
126 | }
127 | }
128 | });
129 | }
130 |
131 | public TimedTextObject loadSubtitle(String path) {
132 | if (TextUtils.isEmpty(path)) {
133 | return null;
134 | }
135 | try {
136 | if (path.startsWith("http://")
137 | || path.startsWith("https://")) {
138 | return loadFromRemote(path);
139 | } else {
140 | return loadFromLocal(path);
141 | }
142 | } catch (Exception e) {
143 | e.printStackTrace();
144 | }
145 | return null;
146 | }
147 |
148 | private static TimedTextObject loadFromRemote(final String remoteSubtitlePath)
149 | throws IOException, FatalParsingException {
150 | Log.d(TAG, "parseRemote: remoteSubtitlePath = " + remoteSubtitlePath);
151 | URL url = new URL(remoteSubtitlePath);
152 | return loadAndParse(url.openStream(), url.getPath());
153 | }
154 |
155 | private static TimedTextObject loadFromLocal(final String localSubtitlePath)
156 | throws IOException, FatalParsingException {
157 | Log.d(TAG, "parseLocal: localSubtitlePath = " + localSubtitlePath);
158 | File file = new File(localSubtitlePath);
159 | return loadAndParse(new FileInputStream(file), file.getPath());
160 | }
161 |
162 | private static TimedTextObject loadAndParse(final InputStream is, final String filePath)
163 | throws IOException, FatalParsingException {
164 | String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
165 | String ext = fileName.substring(fileName.lastIndexOf("."));
166 | Log.d(TAG, "parse: name = " + fileName + ", ext = " + ext);
167 | if (".srt".equalsIgnoreCase(ext)) {
168 | return new FormatSRT().parseFile(fileName, is);
169 | } else if (".ass".equalsIgnoreCase(ext)) {
170 | return new FormatASS().parseFile(fileName, is);
171 | } else if (".stl".equalsIgnoreCase(ext)) {
172 | return new FormatSTL().parseFile(fileName, is);
173 | } else if (".ttml".equalsIgnoreCase(ext)) {
174 | return new FormatSTL().parseFile(fileName, is);
175 | }
176 | return new FormatSRT().parseFile(fileName, is);
177 | }
178 |
179 | public interface Callback {
180 | void onSuccess(TimedTextObject timedTextObject);
181 |
182 | void onError(Exception exception);
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/subtitle/src/main/java/com/avery/subtitle/UIRenderTask.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) of Avery
3 | *
4 | * _ooOoo_
5 | * o8888888o
6 | * 88" . "88
7 | * (| -_- |)
8 | * O\ = /O
9 | * ____/`- -'\____
10 | * .' \\| |// `.
11 | * / \\||| : |||// \
12 | * / _||||| -:- |||||- \
13 | * | | \\\ - /// | |
14 | * | \_| ''\- -/'' | |
15 | * \ .-\__ `-` ___/-. /
16 | * ___`. .' /- -.- -\ `. . __
17 | * ."" '< `.___\_<|>_/___.' >'"".
18 | * | | : `- \`.;`\ _ /`;.`/ - ` : | |
19 | * \ \ `-. \_ __\ /__ _/ .-` / /
20 | * ======`-.____`-.___\_____/___.-`____.-'======
21 | * `=- -='
22 | * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | * Buddha bless, there will never be bug!!!
24 | */
25 |
26 | package com.avery.subtitle;
27 |
28 | import com.avery.subtitle.model.Subtitle;
29 | import com.avery.subtitle.runtime.AppTaskExecutor;
30 |
31 | /**
32 | * @author AveryZhong.
33 | */
34 |
35 | public class UIRenderTask implements Runnable {
36 |
37 | private Subtitle mSubtitle;
38 | private SubtitleEngine.OnSubtitleChangeListener mOnSubtitleChangeListener;
39 |
40 | public UIRenderTask(final SubtitleEngine.OnSubtitleChangeListener l) {
41 | mOnSubtitleChangeListener = l;
42 | }
43 |
44 | @Override
45 | public void run() {
46 | if (mOnSubtitleChangeListener != null) {
47 | mOnSubtitleChangeListener.onSubtitleChanged(mSubtitle);
48 | }
49 | }
50 |
51 | public void execute(final Subtitle subtitle) {
52 | mSubtitle = subtitle;
53 | AppTaskExecutor.mainThread().execute(this);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/subtitle/src/main/java/com/avery/subtitle/cache/SubtitleCache.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) of Avery
3 | *
4 | * _ooOoo_
5 | * o8888888o
6 | * 88" . "88
7 | * (| -_- |)
8 | * O\ = /O
9 | * ____/`- -'\____
10 | * .' \\| |// `.
11 | * / \\||| : |||// \
12 | * / _||||| -:- |||||- \
13 | * | | \\\ - /// | |
14 | * | \_| ''\- -/'' | |
15 | * \ .-\__ `-` ___/-. /
16 | * ___`. .' /- -.- -\ `. . __
17 | * ."" '< `.___\_<|>_/___.' >'"".
18 | * | | : `- \`.;`\ _ /`;.`/ - ` : | |
19 | * \ \ `-. \_ __\ /__ _/ .-` / /
20 | * ======`-.____`-.___\_____/___.-`____.-'======
21 | * `=- -='
22 | * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | * Buddha bless, there will never be bug!!!
24 | */
25 |
26 | package com.avery.subtitle.cache;
27 |
28 | import android.support.annotation.Nullable;
29 |
30 | import com.avery.subtitle.model.Subtitle;
31 |
32 | import java.math.BigInteger;
33 | import java.security.MessageDigest;
34 | import java.util.HashMap;
35 | import java.util.List;
36 |
37 | /**
38 | * @author AveryZhong.
39 | */
40 |
41 | public class SubtitleCache {
42 |
43 | private HashMap> mCache = new HashMap<>();
44 |
45 | public synchronized void put(String key, List subtitles) {
46 | String md5Key = getMD5(key);
47 | if (md5Key == null) {
48 | return;
49 | }
50 | mCache.put(md5Key, subtitles);
51 | }
52 |
53 | @Nullable
54 | public List get(String key) {
55 | String md5Key = getMD5(key);
56 |
57 | if (md5Key == null) {
58 | return null;
59 | }
60 | return mCache.get(md5Key);
61 | }
62 |
63 | private static String getMD5(String str) {
64 | if (str == null) {
65 | return null;
66 | }
67 | try {
68 | MessageDigest md = MessageDigest.getInstance("MD5");
69 | md.update(str.getBytes());
70 | return new BigInteger(1, md.digest()).toString(16);
71 | } catch (Exception e) {
72 | e.printStackTrace();
73 | return null;
74 | }
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/subtitle/src/main/java/com/avery/subtitle/exception/FatalParsingException.java:
--------------------------------------------------------------------------------
1 | package com.avery.subtitle.exception;
2 |
3 | public class FatalParsingException extends Exception {
4 |
5 | private static final long serialVersionUID = 6798827566637277804L;
6 |
7 | private String parsingError;
8 |
9 | public FatalParsingException(String parsingError){
10 | super(parsingError);
11 | this.parsingError = parsingError;
12 | }
13 |
14 | @Override
15 | public String getLocalizedMessage(){
16 | return parsingError;
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/subtitle/src/main/java/com/avery/subtitle/format/FormatASS.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Class that represents the .ASS and .SSA subtitle file format
3 | *
4 | *
5 | * Copyright (c) 2012 J. David Requejo
6 | * j[dot]david[dot]requejo[at] Gmail
7 | *
8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
9 | * and associated documentation files (the "Software"), to deal in the Software without restriction,
10 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
11 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
12 | * is furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all copies
15 | * or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
18 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
19 | * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
20 | * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
21 | * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 | * DEALINGS IN THE SOFTWARE.
23 | *
24 | * @author J. David REQUEJO
25 | *
26 | */
27 |
28 | package com.avery.subtitle.format;
29 |
30 |
31 | import com.avery.subtitle.model.Style;
32 | import com.avery.subtitle.model.Subtitle;
33 | import com.avery.subtitle.model.Time;
34 | import com.avery.subtitle.model.TimedTextObject;
35 |
36 | import java.io.BufferedReader;
37 | import java.io.IOException;
38 | import java.io.InputStream;
39 | import java.io.InputStreamReader;
40 | import java.util.ArrayList;
41 | import java.util.Iterator;
42 |
43 | public class FormatASS implements TimedTextFileFormat {
44 |
45 | public TimedTextObject parseFile(String fileName, InputStream is) throws IOException {
46 |
47 | TimedTextObject tto = new TimedTextObject();
48 | tto.fileName = fileName;
49 |
50 | Subtitle caption = new Subtitle();
51 | Style style;
52 |
53 | //for the clock timer
54 | float timer = 100;
55 |
56 | //if the file is .SSA or .ASS
57 | boolean isASS = false;
58 |
59 | //variables to store the formats
60 | String[] styleFormat;
61 | String[] dialogueFormat;
62 |
63 | //first lets load the file
64 | InputStreamReader in= new InputStreamReader(is);
65 | BufferedReader br = new BufferedReader(in);
66 |
67 | String line;
68 | int lineCounter = 0;
69 | try {
70 | //we scour the file
71 | line=br.readLine();
72 | lineCounter++;
73 | while (line!=null){
74 | line = line.trim();
75 | //we skip any line until we find a section [section name]
76 | if(line.startsWith("[")){
77 | //now we must identify the section
78 | if(line.equalsIgnoreCase("[Script info]")){
79 | //its the script info section section
80 | lineCounter++;
81 | line=br.readLine().trim();
82 | //Each line is scanned for useful info until a new section is detected
83 | while (!line.startsWith("[")){
84 | if(line.startsWith("Title:"))
85 | //We have found the title
86 | tto.title = line.split(":")[1].trim();
87 | else if (line.startsWith("Original Script:"))
88 | //We have found the author
89 | tto.author = line.split(":")[1].trim();
90 | else if (line.startsWith("Script Type:")){
91 | //we have found the version
92 | if(line.split(":")[1].trim().equalsIgnoreCase("v4.00+"))isASS = true;
93 | //we check the type to set isASS or to warn if it comes from an older version than the studied specs
94 | else if(!line.split(":")[1].trim().equalsIgnoreCase("v4.00"))
95 | tto.warnings+="Script version is older than 4.00, it may produce parsing errors.";
96 | } else if (line.startsWith("Timer:"))
97 | //We have found the timer
98 | timer = Float.parseFloat(line.split(":")[1].trim().replace(',','.'));
99 | //we go to the next line
100 | lineCounter++;
101 | line=br.readLine().trim();
102 | }
103 |
104 | } else if (line.equalsIgnoreCase("[v4 Styles]")
105 | || line.equalsIgnoreCase("[v4 Styles+]")
106 | || line.equalsIgnoreCase("[v4+ Styles]")){
107 | //its the Styles description section
108 | if(line.contains("+")&&isASS==false){
109 | //its ASS and it had not been noted
110 | isASS=true;
111 | tto.warnings+="ScriptType should be set to v4:00+ in the [Script Info] section.\n\n";
112 | }
113 | lineCounter++;
114 | line=br.readLine().trim();
115 | //the first line should define the format
116 | if(!line.startsWith("Format:")){
117 | //if not, we scan for the format.
118 | tto.warnings+="Format: (format definition) expected at line "+line+" for the styles section\n\n";
119 | while (!line.startsWith("Format:")){
120 | lineCounter++;
121 | line=br.readLine().trim();;
122 | }
123 | }
124 | // we recover the format's fields
125 | styleFormat = line.split(":")[1].trim().split(",");
126 | lineCounter++;
127 | line=br.readLine().trim();
128 | // we parse each style until we reach a new section
129 | while (!line.startsWith("[")){
130 | //we check it is a style
131 | if (line.startsWith("Style:")){
132 | //we parse the style
133 | style = parseStyleForASS(line.split(":")[1].trim().split(","),styleFormat,lineCounter,isASS,tto.warnings);
134 | //and save the style
135 | tto.styling.put(style.iD, style);
136 | }
137 | //next line
138 | lineCounter++;
139 | line=br.readLine().trim();
140 | }
141 |
142 | } else if (line.trim().equalsIgnoreCase("[Events]")){
143 | //its the events specification section
144 | lineCounter++;
145 | line=br.readLine().trim();
146 | tto.warnings+="Only dialogue events are considered, all other events are ignored.\n\n";
147 | //the first line should define the format of the dialogues
148 | if(!line.startsWith("Format:")){
149 | //if not, we scan for the format.
150 | tto.warnings+="Format: (format definition) expected at line "+line+" for the events section\n\n";
151 | while (!line.startsWith("Format:")){
152 | lineCounter++;
153 | line=br.readLine().trim();
154 | }
155 | }
156 | // we recover the format's fields
157 | dialogueFormat = line.split(":")[1].trim().split(",");
158 | //next line
159 | lineCounter++;
160 | line=br.readLine().trim();
161 | // we parse each style until we reach a new section
162 | while (!line.startsWith("[")){
163 | //we check it is a dialogue
164 | //WARNING: all other events are ignored.
165 | if (line.startsWith("Dialogue:")){
166 | //we parse the dialogue
167 | caption = parseDialogueForASS(line.split(":",2)[1].trim().split(",",10),dialogueFormat,timer, tto);
168 | //and save the caption
169 | int key = caption.start.mseconds;
170 | //in case the key is already there, we increase it by a millisecond, since no duplicates are allowed
171 | while (tto.captions.containsKey(key)) key++;
172 | tto.captions.put(key, caption);
173 | }
174 | //next line
175 | lineCounter++;
176 | line=br.readLine().trim();
177 | }
178 |
179 | } else if (line.trim().equalsIgnoreCase("[Fonts]") || line.trim().equalsIgnoreCase("[Graphics]")){
180 | //its the custom fonts or embedded graphics section
181 | //these are not supported
182 | tto.warnings+= "The section "+line.trim()+" is not supported for conversion, all information there will be lost.\n\n";
183 | line=br.readLine().trim();
184 | } else {
185 | tto.warnings+= "Unrecognized section: "+line.trim()+" all information there is ignored.";
186 | line=br.readLine().trim();
187 | }
188 | } else {
189 | line = br.readLine();
190 | lineCounter++;
191 | }
192 | }
193 | // parsed styles that are not used should be eliminated
194 | tto.cleanUnusedStyles();
195 |
196 | } catch (NullPointerException e){
197 | tto.warnings+= "unexpected end of file, maybe last caption is not complete.\n\n";
198 | } finally{
199 | //we close the reader
200 | if (is != null) {
201 | is.close();
202 | }
203 | }
204 |
205 | tto.built = true;
206 | return tto;
207 | }
208 |
209 |
210 | public String[] toFile(TimedTextObject tto) {
211 |
212 | //first we check if the TimedTextObject had been built, otherwise...
213 | if(!tto.built)
214 | return null;
215 |
216 | //we will write the lines in an ArrayList
217 | int index = 0;
218 | //the minimum size of the file is the number of captions and styles + lines for sections and formats and the script info, so we'll take some extra space.
219 | ArrayList file = new ArrayList(30+tto.styling.size()+tto.captions.size());
220 |
221 | //header is placed
222 | file.add(index++,"[Script Info]");
223 | //title next
224 | String title = "Title: ";
225 | if (tto.title == null || tto.title.isEmpty())
226 | title += tto.fileName;
227 | else title += tto.title;
228 | file.add(index++,title);
229 | //author next
230 | String author = "Original Script: ";
231 | if (tto.author == null || tto.author.isEmpty())
232 | author += "Unknown";
233 | else author += tto.author;
234 | file.add(index++,author);
235 | //additional info
236 | if (tto.copyrigth != null && !tto.copyrigth.isEmpty())
237 | file.add(index++,"; "+tto.copyrigth);
238 | if (tto.description != null && !tto.description.isEmpty())
239 | file.add(index++,"; "+tto.description);
240 | file.add(index++,"; Converted by the Online Subtitle Converter developed by J. David Requejo");
241 | //mandatory info
242 | if (tto.useASSInsteadOfSSA)
243 | file.add(index++,"Script Type: V4.00+");
244 | else file.add(index++,"Script Type: V4.00");
245 | file.add(index++,"Collisions: Normal");
246 | file.add(index++,"Timer: 100,0000");
247 | if (tto.useASSInsteadOfSSA)
248 | file.add(index++,"WrapStyle: 1");
249 | //an empty line is added
250 | file.add(index++,"");
251 |
252 | //Styles section
253 | if (tto.useASSInsteadOfSSA)
254 | file.add(index++,"[V4+ Styles]");
255 | else file.add(index++,"[V4 Styles]");
256 | //define the format
257 | if (tto.useASSInsteadOfSSA)
258 | file.add(index++,"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
259 | else file.add(index++,"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding");
260 | //Next we iterate over the styles
261 | Iterator