极客英语 -(英语学习App)

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后端云

App:https://www.pgyer.com/0DK5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值