1.集成了bmob sdk
2.使用有道在线翻译服务,实现英语单词的翻译功能
3.实现英语文章的在线播放,暂停,列表播放联动,下一句,上一句功能
4.实现单个单词点击popwindow弹框解析显示功能,英/美双语切换功能,中/英切换显示功能
5.采用二分法算法实现当前播放的字幕与音频文件的对应播放
6.Android MediaPlayer媒体播放组件的统一封装,手机来电,去电的广播监听实现播放暂停
7.实现列表单句循环播放功能,常驻通知实现退出App后台播放功能
8.在线英语视频播放功能解析,Android原生TTS语音播放使用示列
9.App夜间模式切换功能实现,仿IOS侧滑退出功能,BaseActivity,BaseFragment的统一封装
10.项目采用kotlin语言+MVVM架构+协程+Jetpack(mvvm框架的封装)
11.技术栈:Kotlin +MVVM + Jetpack + Repository + Retrofit2 + Okhttp3 + Flow + Coroutines
12.英语学习软件的核心重难点功能已实现,后续功能可在此基础进行迭代开发.
项目截图:
英语听力部分代码:
lateinit var adapter : SubtitleRecycleAdapter
private lateinit var datas: ArrayList<SubtitlesBean>
private val mActivity by lazy {
mLifecycleOwner as AudioPlayActivity //类型强制转化
}
private lateinit var manager:LinearLayoutManager
private var phoneBeforeIsPlaying:Boolean = false //来电/去电前是否是播放状态
//单句循环
private var singleSentenceLoop:Boolean = false
val sdf = SimpleDateFormat("mm:ss")
//顶部标题栏高度
var topBarHeight:Int = 0
//底部布局高度
private var bottomHeight:Int = 0
private var visiMode:EnumVisi = EnumVisi.ALL
private var speed:Float = 1.0f
//任务执行器循环周期(毫秒)
private val peroid : Long = 200
//上一个字幕读取的位置
var lastPosition:Int = 0
override fun initView() {
mBinding.viewmodel = this
/**
* 标题栏设置
*/
mBinding.include.title = "听力详情"
mBinding.include.background = mActivity.resources.getColor(R.color.color_app_theme)
//获取标题栏高度
topBarHeight = mBinding.include.root.layoutParams.height
//重新测量
mBinding.bottom.measure(View.MeasureSpec.UNSPECIFIED,View.MeasureSpec.UNSPECIFIED)
bottomHeight = mBinding.bottom.measuredHeight
/**
* 初始化音频管理器
*/
audioManager = App.instance.getSystemService(Context.AUDIO_SERVICE) as AudioManager
mAudioFocusListener = MyOnAudioFocusChangeListener()
/**
* recycleview设置滑动监听
*/
mBinding.recycleview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
System.out.println("newState:"+ newState)
//滑动过程中停止循环任务执行器
if(newState==RecyclerView.SCROLL_STATE_IDLE){
//手指离开后1s,开始执行
mHandler.postDelayed(Runnable {
if(AudioPlayerController.get().isPlaying())startExecutorUpdate()
},1000)
}else{
stopExecutorUpdate()
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
}
})
/**
* seekbar滑动监听
*/
mBinding.seekbar.setOnSeekBarChangeListener(object :SeekBar.OnSeekBarChangeListener{
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
AudioPlayerController.get().seekToPosition(seekBar?.progress?:0)
}
})
/**
* 底部popupwindow弹框
*/
initPopWindow()
/**
* 注册播放状态监听
*/
AudioPlayerController.get().registerCallback(object : AudioPlayerController.Callbacks{
override fun onPlaybackStateChanged(state: AudioPlayerController.PlaybackState?) {
when(state){
AudioPlayerController.PlaybackState.PLAYBACK_STATE_NONE ->{
//停止任务调度
stopExecutorUpdate()
}
AudioPlayerController.PlaybackState.PLAYBACK_STATE_PAUSED ->{
//停止任务调度
stopExecutorUpdate()
}
AudioPlayerController.PlaybackState.PLAYBACK_STATE_PLAYING->{
//开启任务调度器,每0.2s循环获取当前播放进度,更新ui
startExecutorUpdate()
/**
* 请求获取音频焦点。获取到焦点后其他音乐app自动暂停音乐播放,反之亦然
*/
audioManager.requestAudioFocus(mAudioFocusListener,AudioManager.STREAM_MUSIC,AudioManager.AUDIOFOCUS_GAIN)
}
AudioPlayerController.PlaybackState.PLAYBACK_STATE_STOPPED->{
//停止任务调度
stopExecutorUpdate()
}
AudioPlayerController.PlaybackState.PLAYBACK_STATE_COMPLETION->{
//停止任务调度
stopExecutorUpdate()
}
AudioPlayerController.PlaybackState.PLAYBACK_STATE_ERROR->{
//停止任务调度
stopExecutorUpdate()
ToasUtils.show("播放失败")
}
}
}
override fun onAudioDataChanged(duration: Int) {
mBinding.seekbar.max = duration
mBinding.endTv.setText(sdf.format(duration))
}
})
}
音频听力播放控制类(MediaPlayer封装):
/**
* 音频听力播放控制类(MediaPlayer封装)
*/
class AudioPlayerController : MediaPlayer.OnPreparedListener,MediaPlayer.OnErrorListener,MediaPlayer.OnCompletionListener{
private var mPlayer:MediaPlayer?
private lateinit var mPlaybackState : PlaybackState
private var mCallbacks : Callbacks? = null
init{
mPlayer = MediaPlayer()
//注册相关回调监听接口
mPlayer?.run {
setOnPreparedListener(this@AudioPlayerController)
setOnErrorListener(this@AudioPlayerController)
setOnCompletionListener(this@AudioPlayerController)
}
}
/**
* 线程安全的单列懒汉式
*/
companion object{
private var instance:AudioPlayerController?=null
get() {
if (field == null) {
field = AudioPlayerController()
}
return field
}
//在Kotlin中,如果你需要将方法声明为同步,需要添加@Synchronized注解。
@Synchronized
fun get():AudioPlayerController{
return instance!!
}
}
override fun onPrepared(mp: MediaPlayer?) {
if(!isPrepareMode){
mp?.start()
setPlaybackState(PlaybackState.PLAYBACK_STATE_PLAYING)
}
mCallbacks.let {
mp?.run {
duration.let {
mCallbacks?.run {
onAudioDataChanged(it)
}
}
}
}
}
override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
setPlaybackState(PlaybackState.PLAYBACK_STATE_ERROR)
return false
}
override fun onCompletion(mp: MediaPlayer?) {
if(mp!=null && !mp.isLooping){
setPlaybackState(PlaybackState.PLAYBACK_STATE_COMPLETION)
}
}
enum class PlaybackState{
PLAYBACK_STATE_NONE,
PLAYBACK_STATE_STOPPED, // State indicating this item is currently stopped.
PLAYBACK_STATE_PAUSED, // State indicating this item is currently paused.
PLAYBACK_STATE_PLAYING, // State indicating this item is currently playing.
PLAYBACK_STATE_COMPLETION,
PLAYBACK_STATE_ERROR,
}
interface Callbacks {
/**
* Override to handle changes in playback state.
*
* @param state The new playback state of the session
*/
fun onPlaybackStateChanged(state: PlaybackState?)
/**
* Override to handle changes to the current audio data.
*/
fun onAudioDataChanged(duration: Int)
}
var isPrepareMode :Boolean = true
/**
* 准备播放数据,准备成功后需调用mPlayer?.start()方法进行播放
*/
fun prepareAudioData(url:String){
isPrepareMode = true
mPlayer?.run {
reset()//重置
setDataSource(url)//设置播放地址
prepareAsync()//异步装备,防止阻塞主线程
}
}
/**
* 准备加载播放
*/
fun startPlay(url:String){
isPrepareMode = false
mPlayer?.run {
reset()//重置
setDataSource(url)//设置播放地址
prepareAsync()//异步装备,防止阻塞主线程
}
}
/**
* 播放
*/
fun play(){
if(!mPlayer?.isPlaying!!){
mPlayer?.start()
setPlaybackState(PlaybackState.PLAYBACK_STATE_PLAYING)
}
}
/**
* 暂停播放
*/
fun pause(){
mPlayer?.pause()
setPlaybackState(PlaybackState.PLAYBACK_STATE_PAUSED)
}
/**
* 停止播放
*/
fun stop(){
if(mPlayer!=null){
mPlayer?.release()
mPlayer = null
setPlaybackState(PlaybackState.PLAYBACK_STATE_STOPPED)
}
}
/**
* 获取当前播放进度,返回毫秒值
*/
fun getCurrentPosition():Int{
return mPlayer?.currentPosition!!
}
/**
*跳转到指定位置播放
*/
fun seekToPosition(msec : Int){
mPlayer?.seekTo(msec)
play()
}
/**
*设置是否循环播放
*/
fun setLooping(isLoop : Boolean){
(mPlayer?.isLooping) = isLoop
}
/**
* 判断当前音频是否在播放中
*/
fun isPlaying():Boolean{
return mPlayer?.isPlaying?:false
}
/**
* service destory时
*/
fun destory(){
stop()
unregisterCallback()
}
Source code: 项目详情-Bmob后端云