| @@ -36,9 +36,11 @@ | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/drawable/theme_splash_bg.xml" value="0.30520833333333336" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout-v23/include_main_learn_center_course_type_title.xml" value="0.4963768115942029" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_course_main.xml" value="0.33" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_exam_learn_spell.xml" value="0.47690217391304346" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_learn_base.xml" value="0.4979166666666667" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_learn_exam.xml" value="0.23632218844984804" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_learn_exam_word.xml" value="0.33" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_learn_spell.xml" value="0.47690217391304346" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_learn_word2.xml" value="0.4979166666666667" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_main.xml" value="0.5" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_splash.xml" value="0.4921875" /> | |||
| @@ -45,7 +45,8 @@ image包: 实现了图片加载的封装 | |||
| 课时后测试将包含 小游戏练习、再测一次、重新学习、下一步(进入学后总测试)或者下一课时(为当前课时的下一课时), | |||
| 需要考虑的是,下一步或者下一课时的判断为循环判断,如,共10课时,现在点击学习的第5个,如果后面几个课时都学习完后,而1课时没有学习完成,下一课时将直接进入1课时的学习, | |||
| 而下一步是在所有课时都学习完成的情况下,直接进入学后总测试的界面。 | |||
| 学前总测试 :不让跳过,必须测,用于进行界面判断 和 统计的判断 | |||
| 课时学前测试: 不让跳过,必须测,不然对统计和判断有影响 | |||
| @@ -14,7 +14,7 @@ android { | |||
| targetSdk androidConfig.target_sdk_version | |||
| versionCode androidConfig.version_code | |||
| versionName androidConfig.version_name | |||
| multiDexEnabled true //解决64k 分包限制 | |||
| testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | |||
| } | |||
| /*构建类型: 定义Gradle在构建阶段和打包应用时使用的某些属性*/ | |||
| @@ -21,15 +21,9 @@ | |||
| <activity | |||
| android:name=".module.learn.LearnCVideoActivity" | |||
| android:exported="true" /> | |||
| <activity | |||
| android:name=".module.learn.LearnExamSpellActivity" | |||
| android:exported="true" /> | |||
| <activity | |||
| android:name=".module.learn.LearnExamActivity" | |||
| android:exported="true" /> | |||
| <activity | |||
| android:name=".module.learn.LearnSpellActivity" | |||
| android:exported="true" /> | |||
| <activity | |||
| android:name=".module.learn.LearnWordActivity" | |||
| android:exported="true" /> | |||
| @@ -8,7 +8,6 @@ import android.text.style.ForegroundColorSpan | |||
| import android.view.ViewGroup | |||
| import android.widget.Toast | |||
| import androidx.core.content.ContextCompat | |||
| import androidx.core.text.isDigitsOnly | |||
| import com.suliang.common.base.adapter.BaseRVAdapter | |||
| import com.suliang.common.base.adapter.BaseAdapterViewHolder | |||
| @@ -57,7 +56,7 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| * @param isOver 纠错是否完成 | |||
| * @param nextPositon 纠错中的下一项 | |||
| */ | |||
| lateinit var onItemRecoveryClick : (showValue : SpannableStringBuilder, isOver:Boolean, nextPosition:Int) -> Unit | |||
| lateinit var onItemRecoveryClick : (showValue : SpannableStringBuilder, isOver : Boolean, nextPosition : Int) -> Unit | |||
| /** | |||
| * 拼写时的事件 | |||
| @@ -67,7 +66,7 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| * @param correctValue 正确的拼写值,但未选中的赋值了颜色 | |||
| * @param errorSize 错误的个数,拼写完成后才会有这个值 | |||
| */ | |||
| lateinit var onItemSpellingClickListener : (selectedValue : SpannableStringBuilder, nextPosition : Int, isOver : Boolean , correctValue : SpannableStringBuilder, errorSize : Int) -> Unit | |||
| lateinit var onItemSpellingClickListener : (selectedValue : SpannableStringBuilder, nextPosition : Int, isOver : Boolean, correctValue : SpannableStringBuilder, errorSize : Int) -> Unit | |||
| override fun coverViewHolder(parent : ViewGroup, viewType : Int) : BaseAdapterViewHolder { | |||
| @@ -89,14 +88,13 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| playItem(item.char) | |||
| item.isSelected = true | |||
| getItem(if (position % 2 == 0) position + 1 else position - 1).isSelected = false | |||
| notifyDataSetChanged() | |||
| notifyUIUpdate(position) | |||
| notifyDataSetChanged() | |||
| } | |||
| } | |||
| } else { //不可拼写时 正确项未选中为红色,否则为白色 | |||
| binding.tv.setBackgroundColor(ContextCompat.getColor(context, | |||
| if (item.isCorrect && !item.isSelected) R.color.red_2 else R.color.white)) | |||
| if (item.isCorrect && !item.isSelected) R.color.red_1 else R.color.white)) | |||
| binding.tv.setOnClickListener { | |||
| //非纠错校正则点击无效 | |||
| if (!isErrorRecovery) return@setOnClickListener | |||
| @@ -106,10 +104,9 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| } else { | |||
| playItem(item.char) | |||
| item.isSelected = true | |||
| notifyItemChanged(position) | |||
| notifyUIUpdate(position) | |||
| notifyItemChanged(position) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -119,19 +116,18 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| */ | |||
| private fun clickInvalid() { | |||
| Toast.makeText(context, "请依次点击", Toast.LENGTH_SHORT).show() | |||
| MPManager.play("common_voice/mistake.mp3") | |||
| MPManager.playAsset("common_voice/mistake.mp3") | |||
| } | |||
| /** item 播放 */ | |||
| private fun playItem(letter : Char) { | |||
| if (letter.isLetter()) { | |||
| when (defaultSoundWay) { | |||
| AppConstants.SOUND_TYPE_UK -> MPManager.play("common_voice_uk/${letter}_uk.mp3") | |||
| AppConstants.SOUND_TYPE_US -> MPManager.play("common_voice_us/${letter}_us.mp3") | |||
| AppConstants.SOUND_TYPE_UK -> MPManager.playAsset("common_voice_uk/${letter}_uk.mp3") | |||
| AppConstants.SOUND_TYPE_US -> MPManager.playAsset("common_voice_us/${letter}_us.mp3") | |||
| } | |||
| } else { | |||
| MPManager.play("common_voice/konge.mp3") | |||
| MPManager.playAsset("common_voice/konge.mp3") | |||
| } | |||
| } | |||
| @@ -144,7 +140,7 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| //第二列 position为 1 3 5 7 9 | |||
| if (position == 0 || position == 1) return true | |||
| //上一列的第一行坐标 | |||
| val previousColumnFirstPosition = if (position % 2 == 0) position - 2 else position - 1 | |||
| val previousColumnFirstPosition = if (position % 2 == 0) position - 2 else position - 3 | |||
| if (getItem(previousColumnFirstPosition).isSelected) return true | |||
| //上一列的第二行坐标 | |||
| val previousColumnSecondPosition = previousColumnFirstPosition + 1 | |||
| @@ -184,14 +180,21 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| //是否是最后一列 | |||
| val isOver = (position == itemCount - 1) || (position == itemCount - 2) | |||
| var errorSize = 0 | |||
| var nextPosition = 0 | |||
| var nextPosition = 0 //下一列的第一行坐标位置 | |||
| //记录正确的值,如果没有选中,则需要设置颜色 | |||
| val correctValue = SpannableStringBuilder() | |||
| if (!isOver) { | |||
| //拼写时,直接赋值 | |||
| getData().forEachIndexed { index, it -> | |||
| if (it.isSelected) builder.append(it.char) | |||
| if (it.isCorrect && (index == position + 1 || index == position + 2)) nextPosition = index | |||
| //滑动到下一列的位置 | |||
| if (position == index) { //点击的位置 | |||
| if (position % 2 == 0) { | |||
| nextPosition = position + 2 | |||
| } else { | |||
| nextPosition = position + 1 | |||
| } | |||
| } | |||
| } | |||
| } else { //拼写完成 | |||
| getData().forEachIndexed { index, it -> | |||
| @@ -209,18 +212,15 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) | |||
| }) | |||
| //错误数加1 | |||
| errorSize+=1 | |||
| //下一个位置定义到第一个错误的位置 | |||
| if (nextPosition == 0 ){ | |||
| nextPosition = index | |||
| } | |||
| errorSize += 1 | |||
| } | |||
| } | |||
| } | |||
| } | |||
| isSpelling = false | |||
| } | |||
| //回调出去 | |||
| onItemSpellingClickListener(builder,nextPosition,isOver,correctValue,errorSize) | |||
| onItemSpellingClickListener(builder, nextPosition, isOver, correctValue, errorSize) | |||
| } | |||
| //纠错: 拼接所有正确的内容,未选中的需要设置颜色 | |||
| @@ -245,12 +245,43 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| //判断是否还有大于当前列第二行坐标的未选,如果有,则为纠错未完成 | |||
| if (index > currentSecondPosition) { | |||
| isOver = false | |||
| nextPosition = index | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| onItemRecoveryClick(builder, isOver,nextPosition) | |||
| onItemRecoveryClick(builder, isOver, nextPosition) | |||
| } | |||
| } | |||
| /** 测试时倒计时结束,进行取值结果回调 */ | |||
| fun testCountingTimeOver() { | |||
| val builder = SpannableStringBuilder() | |||
| val correctValue = SpannableStringBuilder() | |||
| var errorSize = 0 | |||
| getData().forEachIndexed { index, it -> | |||
| //记录拼写的值 | |||
| if (it.isSelected) builder.append(it.char) | |||
| //记录正确的值 | |||
| when { | |||
| it.isCorrect -> when { | |||
| it.isSelected -> correctValue.append(it.char) | |||
| else -> { | |||
| correctValue.append(SpannableString(it.char.toString()).apply { | |||
| setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.theme_color)), | |||
| 0, | |||
| 1, | |||
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) | |||
| }) | |||
| //错误数加1 | |||
| errorSize += 1 | |||
| } | |||
| } | |||
| } | |||
| } | |||
| isSpelling = false | |||
| onItemSpellingClickListener(builder, 0, true, correctValue, errorSize) | |||
| notifyDataSetChanged() | |||
| } | |||
| } | |||
| @@ -1,11 +1,10 @@ | |||
| package com.xkl.cdl.adapter.itemdecoration | |||
| import android.content.Context | |||
| import android.graphics.Rect | |||
| import android.view.View | |||
| import com.suliang.common.util.os.ScreenUtil | |||
| import androidx.recyclerview.widget.RecyclerView.ItemDecoration | |||
| import androidx.recyclerview.widget.RecyclerView | |||
| import androidx.recyclerview.widget.RecyclerView.ItemDecoration | |||
| import com.suliang.common.util.os.ScreenUtil | |||
| /** | |||
| * Created by su on 2018/11/26. | |||
| @@ -13,67 +12,37 @@ import androidx.recyclerview.widget.RecyclerView | |||
| */ | |||
| class SpellItemDecoration : ItemDecoration() { | |||
| private var mDividerWidth = ScreenUtil.dp2px(1f) | |||
| private val mDividerWidth = ScreenUtil.dp2px(1f) | |||
| override fun getItemOffsets(outRect : Rect, view : View, parent : RecyclerView, state : RecyclerView.State) { | |||
| val space = mDividerWidth/2 | |||
| val chilidCount = parent.adapter!!.itemCount | |||
| val position = (view.layoutParams as RecyclerView.LayoutParams).viewLayoutPosition | |||
| //获取view所在的位置(从0计数) | |||
| val position = parent.getChildAdapterPosition(view) | |||
| var left = 0 | |||
| var top = 0 | |||
| var right = 0 | |||
| var top = 0 | |||
| var right = 0 | |||
| var bottom = 0 | |||
| when{ | |||
| position % 2 == 0 -> { //第一行 | |||
| position % 2 == 0 -> { //第一行 | |||
| left = mDividerWidth | |||
| top = mDividerWidth | |||
| bottom = space | |||
| when{ | |||
| position == 0 && position == chilidCount - 2 -> { //只有一列 | |||
| left = mDividerWidth | |||
| right = mDividerWidth | |||
| } | |||
| position == 0 -> { //第一列 | |||
| left = mDividerWidth | |||
| right = space | |||
| } | |||
| position == chilidCount - 2 -> { //最后一列 | |||
| left = space | |||
| right = mDividerWidth | |||
| } | |||
| else -> { | |||
| left = space | |||
| right = space | |||
| } | |||
| } | |||
| bottom = mDividerWidth | |||
| right = 0 | |||
| } | |||
| else -> { //第二行 | |||
| top = space | |||
| else -> { //第二行 | |||
| left = mDividerWidth | |||
| top = 0 | |||
| bottom = mDividerWidth | |||
| when{ | |||
| position == 1 && position == chilidCount - 1 -> { //只有一列 | |||
| left = mDividerWidth | |||
| right = mDividerWidth | |||
| } | |||
| position == 1 -> { //第一列 | |||
| left = mDividerWidth | |||
| right = space | |||
| } | |||
| position == chilidCount - 2 -> { //最后一列 | |||
| left = space | |||
| right = mDividerWidth | |||
| } | |||
| else -> { | |||
| left = space | |||
| right = space | |||
| } | |||
| } | |||
| right = 0 | |||
| } | |||
| } | |||
| //最后一列 | |||
| if (position == chilidCount - 1 || position == chilidCount - 2){ | |||
| right = mDividerWidth | |||
| } | |||
| outRect.set(left,top,right, bottom) | |||
| } | |||
| @@ -149,21 +149,44 @@ object AppConstants { | |||
| /**测试错误: 未答到一题的时间*/ | |||
| const val TEST_TO_NEXT_ERROR_TIME = 2000L | |||
| /** 对话框类型: 测试 */ | |||
| const val DIALOG_TYPE_EXAM = 1 | |||
| /** 对话框类型: 学习 */ | |||
| const val DIALOG_TYPE_LEARN = 2 | |||
| /** 对话框类型: 测试开始弹窗 与 结束弹窗 */ | |||
| const val DIALOG_TYPE_EXAM_START = 1 | |||
| const val DIALOG_TYPE_EXAM_OVER = 2 | |||
| /** 对话框类型: 学习结束弹窗类型 */ | |||
| const val DIALOG_TYPE_LEARN_OVER = 3 | |||
| /**--- 总线动作 --------------------------------- */ | |||
| /**action key 改变界面 到目录页 */ | |||
| const val EVENT_COURSE = "action_change_page" | |||
| /** 动作:学前总测之 开始学习 */ | |||
| /** 事件动作:学前总测结束弹窗之 开始学习 */ | |||
| const val ACTION_COURSE_TEST_START_LEARN = 1 | |||
| /** 数据动作:学前总测结束传递数据 */ | |||
| const val DATA_COURSE_TEST_BEFORE = 2 | |||
| const val DATA_COURSE_BEFORE_TEST_OVER = 2 | |||
| /** 数据动作:课时学前测试结束传递数据 */ | |||
| const val DATA_LESSON_BEFORE_TEST_OVER = 3 | |||
| /** 课时学前测试结束 : 开始学习 */ | |||
| const val ACTION_LESSON_BEFORE_TEST_OVER_START_LEARN = 4 | |||
| /**数据动作: 课时学后测试结束传递数据*/ | |||
| const val DATA_LESSON_AFTER_TEST_OVER = 5 | |||
| /**课时学后测试结束发送动作: 重新学习*/ | |||
| const val ACTION_LESSON_AFTER_TEST_RELEARN = 6 | |||
| /**课时学后测试弹窗动作: 再测一次*/ | |||
| const val ACTION_LESSON_AFTER_TEST_AGAIN = 7 | |||
| /**课时学后测试弹窗动作: 下一步*/ | |||
| const val ACTION_LESSON_AFTER_TEST_NEXT = 8 | |||
| /**--- 弹窗动作 --------------------------------- */ | |||
| /** 学前总测弹窗: 开始学习 */ | |||
| /** 学前总测结束弹窗: 开始学习 ,课时学前测试开始弹窗*/ | |||
| const val DIALOG_START_LEARN = 1 | |||
| /** 弹窗按钮动作:开始测试 */ | |||
| const val DIALOG_START_TEST = 2 | |||
| /**课时学后测试弹窗动作: 重新学习*/ | |||
| const val DIALOG_LESSON_AFTER_TEST_RELEARN = 3 | |||
| /**课时学后测试弹窗动作: 再测一次*/ | |||
| const val DIALOG_LESSON_AFTER_TEST_AGAIN = 4 | |||
| /**课时学后测试弹窗动作: 下一步*/ | |||
| const val DIALOG_LESSON_AFTER_TEST_NEXT = 5 | |||
| } | |||
| @@ -1,12 +0,0 @@ | |||
| package com.xkl.cdl.data.bean | |||
| import com.xkl.cdl.data.bean.course.Lesson | |||
| /** | |||
| * author suliang | |||
| * create 2022/4/2 14:55 | |||
| * Describe: 课时学习时的传参 | |||
| */ | |||
| class IntentLearnData(val lesson: Lesson) { | |||
| } | |||
| @@ -7,11 +7,14 @@ import android.os.Parcelable | |||
| * author suliang | |||
| * create 2022/4/14 9:37 | |||
| * Describe: 学习测试对话框--传参实体 | |||
| * | |||
| * 所有的测试必传测试类型 [examType] | |||
| * | |||
| * 学前总测 :[examType],[score],[correctNumber],[errorNumber] | |||
| * 学前总测结束 :[examType],[score],[correctNumber],[errorNumber] | |||
| * | |||
| * 课时学前测试开始: [examType],[chapter_lesson_name],[showTimeCount] | |||
| * | |||
| * 课时学前测试 | |||
| * 课时学前测试结束:[examType],[score],[correctNumber],[errorNumber] | |||
| * | |||
| * 课时学后测试 | |||
| * | |||
| @@ -38,6 +41,11 @@ class LearnDialogBean(val dialogType:Int) : Parcelable { | |||
| /** 错误数据 */ | |||
| var errorNumber : Int = 0 | |||
| /**章节课时名称: 第一章 第二课时 */ | |||
| var chapter_lesson_name = "" | |||
| /** 题数与时间 */ | |||
| var showTimeCount = "" | |||
| @@ -1,5 +1,7 @@ | |||
| package com.xkl.cdl.data.bean | |||
| import com.xkl.cdl.data.bean.BaseWord | |||
| /** | |||
| * author suliang | |||
| * create 2022/3/31 17:52 | |||
| @@ -30,15 +32,15 @@ class LearnWord(subjectId: Int, | |||
| var phonetic_uk: String? = null // 英式音标 | |||
| var phonetic_us: String? = null // 美式音标 | |||
| var phonetic_cn: String? = null // 中文拼音 | |||
| var photo: String? = null // 图片 | |||
| var audio: String? = null // 音频 | |||
| // var photo: ByteArray? = null // 图片 | |||
| var basic_explanation: String? = null //基本释义 | |||
| var all_explanation: String? = null //扩展释义 | |||
| var extend_explanation: String? = null //扩展释义 | |||
| var phrase: String? = null //词组 | |||
| var example: String? = null //例句 | |||
| var reference: String? = null //参考 | |||
| var literacyIspolyphone = false //识字课程,是否是多音字标识 | |||
| var pattern: String? = null //口语: 语法 | |||
| } | |||
| @@ -1,7 +1,5 @@ | |||
| package com.xkl.cdl.data.bean.course | |||
| import android.R.bool | |||
| /** | |||
| * author suliang | |||
| @@ -18,9 +18,20 @@ class ExamBean { | |||
| var chapterId : Long = 0 //章节id | |||
| var lessonId : Long = 0 //课时id | |||
| //口语 | |||
| var questionIsAudio = false //问题是否是音频 | |||
| var answersIsAudio = false //答案是否是音频 | |||
| //口语总测的发音 | |||
| var wordAudioUk : String = "" | |||
| var wordAudioUs : String = "" | |||
| var correctAudioUk : String = "" | |||
| var correctAudioUs : String = "" | |||
| var error1AudioUk : String = "" | |||
| var error1AudioUs : String = "" | |||
| var error2AudioUk : String = "" | |||
| var error2AudioUs : String = "" | |||
| var error3AudioUk : String = "" | |||
| var error3AudioUs : String = "" | |||
| } | |||
| @@ -13,6 +13,7 @@ import com.xkl.cdl.data.AppConstants | |||
| data class Lesson( | |||
| val subjectId: Int, | |||
| val coursePackId:Long, | |||
| val coursePackType:Int, | |||
| val courseId:Long, | |||
| var courseType: Int, | |||
| val chapterId:Long, | |||
| @@ -26,8 +27,8 @@ data class Lesson( | |||
| var lessonPositionInList : Int = 0 | |||
| /** 该课时的总数据 或者作文关联的数据,如视频关联的内容*/ | |||
| var wordIds = mutableListOf<Long>() | |||
| /** 学习进度位置,为医学下标的位置,在作文课堂练习时,学习取当前条,其他取下一条*/ | |||
| var learnedIndex: Int = 0 | |||
| /** 学习进度位置,为已学下标的位置,在作文课堂练习时,学习取当前条,其他取下一条*/ | |||
| var learnedIndex: Int = -1 | |||
| /**课时学前测试成绩 */ | |||
| var beforeTestScore = -1.0 | |||
| /** 课时学后测试 */ | |||
| @@ -1,17 +1,19 @@ | |||
| package com.xkl.cdl.data.bean.intentdata | |||
| import com.xkl.cdl.data.bean.course.DialogueSpokenItem | |||
| import com.xkl.cdl.data.bean.course.ExamBean | |||
| import com.xkl.cdl.data.bean.course.Lesson | |||
| import kotlin.collections.HashMap | |||
| /** | |||
| * author suliang | |||
| * create 2022/4/8 12:01 | |||
| * Describe: 测试传递数据 | |||
| * | |||
| * 学前总测: coursePackType, courseType, testData ,mExamWRMap | |||
| * | |||
| * 学后总测: coursePackType, courseType, testData | |||
| * | |||
| * 课时学前测: coursePackType, courseType, testData ,mExamWRMap,lesson | |||
| * | |||
| * 课时学后测: coursePackType, courseType, testData ,lesson. | |||
| * | |||
| */ | |||
| @@ -19,7 +21,10 @@ data class ExamData( | |||
| val subjectId : Int, //项目类型 | |||
| val examType : Int, //测试类型 | |||
| val showTitle : String, //去测试本地显示名称,学前学后 为课程名称,课时前、后为课时名称 | |||
| val saveTitle : String, //测试试卷上传名称 : 学习中心:课程名称 + “ ” + 学习类型 课程测试:课程名称 + “ ” 学习类型 + (章节拼接名称) 词汇量测试:词汇量测试:阶段名称 | |||
| //测试试卷上传名称 : 学习中心:“课程名称 章节名称 课时名称” | |||
| // 课程测试:课程名称 + “ ” 学习类型 + (章节拼接名称) | |||
| // 词汇量测试:词汇量测试:阶段名称 | |||
| val saveTitle : String, | |||
| ) { | |||
| var coursePackId : Long = 0 //课程包id, 测试错误上次数据需要 | |||
| var coursePackType:Int = 0 //课程包类型 | |||
| @@ -0,0 +1,15 @@ | |||
| package com.xkl.cdl.data.bean.intentdata | |||
| import com.xkl.cdl.data.bean.LearnWord | |||
| import com.xkl.cdl.data.bean.course.Lesson | |||
| /** | |||
| * author suliang | |||
| * create 2022/4/2 14:55 | |||
| * Describe: 课时学习时的传参 | |||
| */ | |||
| class LearnData(val lesson: Lesson) { | |||
| /**学习数据*/ | |||
| var learnWordList : List<LearnWord> = mutableListOf() | |||
| } | |||
| @@ -6,7 +6,7 @@ package com.xkl.cdl.data.event | |||
| * Describe: 事件通知更新实体封装 | |||
| * @param subjectId 项目id | |||
| * @param courseId 课程id | |||
| * @param actionFlag 动作id 动作 或 传递数据 | |||
| * @param actionFlag 动作id 动作(action开头) 或 传递数据(data开头) | |||
| */ | |||
| class LearnEventData(val subjectId : Int, var courseId : Long, val actionFlag : Int) { | |||
| @@ -15,5 +15,7 @@ class LearnEventData(val subjectId : Int, var courseId : Long, val actionFlag : | |||
| var scoreValue = 0 //分数 | |||
| var newErrorMap : HashMap<String, Boolean>? = null //新错误单词列表 | |||
| //课时学前测试 除了学前总测传递数据,还需要课时位置数据, | |||
| var leesonPositionIndex = 0 // lesson所在位置 | |||
| } | |||
| @@ -2,15 +2,23 @@ package com.xkl.cdl.data.manager.db | |||
| import android.annotation.SuppressLint | |||
| import android.widget.Toast | |||
| import androidx.core.database.getBlobOrNull | |||
| import androidx.core.database.getLongOrNull | |||
| import androidx.core.database.getStringOrNull | |||
| import com.google.common.base.Joiner | |||
| import com.suliang.common.AppConfig | |||
| import com.suliang.common.util.AppGlobals | |||
| import com.suliang.common.util.file.FileUtil | |||
| import com.suliang.common.util.thread.AppExecutors | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.bean.LearnWord | |||
| import com.xkl.cdl.data.bean.course.CourseDetail | |||
| import com.xkl.cdl.data.bean.course.ExamBean | |||
| import com.xkl.cdl.data.bean.course.Lesson | |||
| import com.xkl.cdl.data.manager.FilePathManager | |||
| import io.reactivex.rxjava3.annotations.NonNull | |||
| import net.sqlcipher.database.SQLiteDatabase | |||
| import java.io.File | |||
| /** | |||
| * author suliang | |||
| @@ -39,15 +47,14 @@ object DBCourseManager { | |||
| AppConstants.COURSE_TYPE_CHINESE_LITERACY -> LITERACY | |||
| else -> NORMAL | |||
| } | |||
| mDataBase = SQLiteDatabase.openDatabase( | |||
| FilePathManager.getCourseDbPath(currentBase).path, p, null, SQLiteDatabase.OPEN_READONLY | |||
| ) | |||
| mDataBase = SQLiteDatabase.openDatabase(FilePathManager.getCourseDbPath(currentBase).path, | |||
| p, | |||
| null, | |||
| SQLiteDatabase.OPEN_READONLY) | |||
| } | |||
| if (mDataBase == null) { | |||
| AppExecutors.mainThread.run { | |||
| Toast.makeText( | |||
| AppGlobals.application, "课程数据获取失败,请重启应用或联系业务员", Toast.LENGTH_LONG | |||
| ) | |||
| Toast.makeText(AppGlobals.application, "课程数据获取失败,请重启应用或联系业务员", Toast.LENGTH_LONG) | |||
| } | |||
| } | |||
| } | |||
| @@ -80,14 +87,16 @@ object DBCourseManager { | |||
| val chapterId : Long = it.getLong(it.getColumnIndex("chapter_id")) | |||
| val chapterName : String = it.getString(it.getColumnIndex("chapter_title")) | |||
| val lessonId = it.getLong(it.getColumnIndex("lesson_id")) | |||
| val lessonName = when (base.courseType) { | |||
| AppConstants.COURSE_TYPE_CHINESE_COMPOSITION -> it.getString(it.getColumnIndex("lesson_title")) | |||
| val lessonName : String = when (base.courseType) { | |||
| AppConstants.COURSE_TYPE_CHINESE_COMPOSITION, AppConstants.COURSE_TYPE_CHINESE_LITERACY, AppConstants.COURSE_TYPE_CHINESE_PINYIN -> it.getString( | |||
| it.getColumnIndex("lesson_title")) | |||
| else -> it.getString(it.getColumnIndex("lesson")) | |||
| } | |||
| val wordIds = when (base.courseType) { | |||
| val wordIds : MutableList<Long> = when (base.courseType) { | |||
| AppConstants.COURSE_TYPE_CHINESE_COMPOSITION -> it.getString(it.getColumnIndex("relation_id")).split(",") | |||
| else -> it.getString(it.getColumnIndex("wordIds")).split(",") | |||
| }.map { value -> value.toLong() }.toMutableList() | |||
| val lessonType : Int = when (base.courseType) { | |||
| AppConstants.COURSE_TYPE_CHINESE_COMPOSITION -> it.getInt(it.getColumnIndex("type")) | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPOKEN -> it.getInt(it.getColumnIndex("lesson_type")) | |||
| @@ -95,22 +104,20 @@ object DBCourseManager { | |||
| } | |||
| val key = "${chapterId}_${lessonId}" | |||
| val learnIndex = wordIds.indexOf( | |||
| detail.lesson_learn_point.getOrElse(key, { -1 }) | |||
| ) //学习位置,当前位置为已学 | |||
| val learnIndex = wordIds.indexOf(detail.lesson_learn_point.getOrElse(key, { -1 })) //学习位置,当前位置为已学 | |||
| // 注:针对口语对话课时,如果没有学习完,是不会设置进度点的。 | |||
| val learnIsOver = wordIds.size - 1 == learnIndex | |||
| val lesson = Lesson( | |||
| base.subjectId, | |||
| base.coursePackId, | |||
| base.courseId, | |||
| base.courseType, | |||
| chapterId, | |||
| chapterName, | |||
| lessonId, | |||
| lessonName).apply { | |||
| val lesson = Lesson(base.subjectId, | |||
| base.coursePackId, | |||
| base.coursePackType, | |||
| base.courseId, | |||
| base.courseType, | |||
| chapterId, | |||
| chapterName, | |||
| lessonId, | |||
| lessonName).apply { | |||
| lessonPositionInList = positionIndex | |||
| this.wordIds = wordIds //内容 | |||
| totalNumber = this.wordIds.size //总数 | |||
| @@ -169,7 +176,8 @@ object DBCourseManager { | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPOKEN -> { //口语 | |||
| when (testType) { | |||
| AppConstants.TEST_TYPE_BEFORE_TOTAL, AppConstants.TEST_TYPE_AFTER_TOTAL -> { //总测 | |||
| "SELECT * FROM exam WHERE type = 5 ORDER by random() LIMIT $count" | |||
| // "SELECT * FROM exam WHERE type = 5 ORDER by random() LIMIT $count" | |||
| "SELECT * FROM exam JOIN exam_res on exam.type = 5 AND exam.word_id = exam_res.exam_spoken_id LIMIT $count" | |||
| } | |||
| AppConstants.TEST_TYPE_BEFORE, AppConstants.TEST_TYPE_AFTER -> { | |||
| "SELECT exam.* FROM chapter JOIN exam on chapter.word_id = exam.word_id AND exam.type = 1 AND chapter_id = ${lesson!!.chapterId} AND lesson_id = ${lesson.lessonId} ORDER by random() LIMIT $count" | |||
| @@ -180,9 +188,7 @@ object DBCourseManager { | |||
| else -> "" //没有任何课程,课程都特别写入类型,用于匹配 | |||
| } | |||
| mDataBase?.rawQuery( | |||
| sql, null | |||
| )?.run { | |||
| mDataBase?.rawQuery(sql, null)?.run { | |||
| when (base.courseType) { | |||
| AppConstants.COURSE_TYPE_ENGLISH_DISCERN, AppConstants.COURSE_TYPE_ENGLISH_VOICE, | |||
| AppConstants.COURSE_TYPE_ENGLISH_SOUNDMARK, AppConstants.COURSE_TYPE_CHINESE_LITERACY, | |||
| @@ -217,21 +223,63 @@ object DBCourseManager { | |||
| } | |||
| } | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPOKEN -> { | |||
| while (moveToNext()) { | |||
| result.add(ExamBean().apply { | |||
| id = getLong(0) | |||
| word_id = getLong(1) | |||
| word = getString(2) | |||
| correct = getString(3) | |||
| error1 = getString(4) | |||
| error2 = getString(5) | |||
| error3 = getString(6) | |||
| type = AppConstants.TEST_QUEST_TYPE_CHOICE // 为1 | |||
| lesson?.let { //课时测试时才会有课时存在 | |||
| chapterId = it.chapterId | |||
| lessonId = it.lessonId | |||
| when (testType) { | |||
| AppConstants.TEST_TYPE_BEFORE_TOTAL, AppConstants.TEST_TYPE_AFTER_TOTAL -> { | |||
| val parentPath = FileUtil.getSaveDirPath(AppConfig.VOICE) | |||
| while (moveToNext()) { | |||
| result.add(ExamBean().apply { | |||
| id = getLong(0) | |||
| word_id = getLong(1) | |||
| word = getString(2) | |||
| correct = getString(3) | |||
| error1 = getString(4) | |||
| error2 = getString(5) | |||
| error3 = getString(6) | |||
| questionIsAudio = getInt(7) > 0 | |||
| answersIsAudio = getInt(8) > 0 | |||
| type = 5 // 口语总测 type=5 | |||
| //总测时将发音字节码写入文件 | |||
| if (!File(parentPath, word).exists()) { //不存在 | |||
| FileUtil.writeBytesToFile(parentPath, word, getBlob(12)) | |||
| FileUtil.writeBytesToFile(parentPath, word, getBlob(13)) | |||
| } | |||
| if (!File(parentPath, correct).exists()) { //不存在 | |||
| FileUtil.writeBytesToFile(parentPath, word, getBlob(14)) | |||
| FileUtil.writeBytesToFile(parentPath, word, getBlob(15)) | |||
| } | |||
| if (!File(parentPath, error1).exists()) { //不存在 | |||
| FileUtil.writeBytesToFile(parentPath, word, getBlob(16)) | |||
| FileUtil.writeBytesToFile(parentPath, word, getBlob(17)) | |||
| } | |||
| if (!File(parentPath, error2).exists()) { //不存在 | |||
| FileUtil.writeBytesToFile(parentPath, word, getBlob(18)) | |||
| FileUtil.writeBytesToFile(parentPath, word, getBlob(19)) | |||
| } | |||
| if (!File(parentPath, error3).exists()) { //不存在 | |||
| FileUtil.writeBytesToFile(parentPath, word, getBlob(20)) | |||
| FileUtil.writeBytesToFile(parentPath, word, getBlob(21)) | |||
| } | |||
| }) | |||
| } | |||
| }) | |||
| } | |||
| else -> { | |||
| while (moveToNext()) { | |||
| result.add(ExamBean().apply { | |||
| id = getLong(0) | |||
| word_id = getLong(1) | |||
| word = getString(2) | |||
| correct = getString(3) | |||
| error1 = getString(4) | |||
| error2 = getString(5) | |||
| error3 = getString(6) | |||
| type = AppConstants.TEST_QUEST_TYPE_CHOICE // 为1 | |||
| lesson?.let { //课时测试时才会有课时存在 | |||
| chapterId = it.chapterId | |||
| lessonId = it.lessonId | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -240,38 +288,164 @@ object DBCourseManager { | |||
| return result | |||
| } | |||
| /** 获取指定课时(指定数据源wordIds)的随机个数测试数据 */ | |||
| fun queryLessonTestRandomSize( | |||
| coursePackId : Long, | |||
| courseId : Long, | |||
| chapterId : Long, | |||
| lessonId : Long, | |||
| courseType : Long, | |||
| limitSize : Int, | |||
| wordIds : ArrayList<Long>? = null, | |||
| ) { | |||
| /** | |||
| * 查询单词的发音音频 | |||
| * @param base DbControlBase | |||
| * @param wordId Long 查询单词 | |||
| * @param soundWay Int 发音方式 | |||
| * @return String 发音保存的文件名称 要不为空,要不为文件地址, 为空,则表明没有发音文件,如果是英美发音方式,则表示英美都没有发音内容 | |||
| */ | |||
| fun queryAudio(base : DbControlBase, wordId : Long, soundWay : Int) : String? { | |||
| return when (base.courseType) { | |||
| // 有英美两种发音 | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPOKEN, //这里的为口语的正常获取 | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL, AppConstants.COURSE_TYPE_ENGLISH_DISCERN, AppConstants.COURSE_TYPE_ENGLISH_SOUNDMARK, AppConstants.COURSE_TYPE_ENGLISH_VOICE -> { | |||
| val sql = "SELECT audio_us,audio_uk FROM res WHERE word_id = $wordId" | |||
| var audio_us : ByteArray? = null | |||
| var audio_uk : ByteArray? = null | |||
| open(base) | |||
| mDataBase?.rawQuery(sql, null)?.let { | |||
| while (it.moveToNext()) { | |||
| //写入文件 | |||
| audio_us = it.getBlobOrNull(0) | |||
| audio_uk = it.getBlobOrNull(1) | |||
| } | |||
| it.close() | |||
| } | |||
| //不为空,写入本身,如果为空,用另外的发音方式写入 | |||
| val parentPath = FileUtil.getSaveDirPath(AppConfig.VOICE) | |||
| val audio_us_file_path = audio_us?.let { | |||
| val audioFileNameUS = | |||
| "${base.subjectId}_${base.coursePackId}_${base.courseId}_${wordId}_${AppConstants.SOUND_TYPE_US}" | |||
| val file = File(parentPath, audioFileNameUS) | |||
| FileUtil.writeBytesToFile(file, it) | |||
| file.path | |||
| } ?: audio_uk?.let { | |||
| val audioFileNameUS = | |||
| "${base.subjectId}_${base.coursePackId}_${base.courseId}_${wordId}_${AppConstants.SOUND_TYPE_US}" | |||
| val file = File(parentPath, audioFileNameUS) | |||
| FileUtil.writeBytesToFile(file, it) | |||
| file.path | |||
| } | |||
| //不为空,写入本身,如果为空,用另外的发音方式写入 | |||
| val audio_uk_file_path = audio_uk?.let { | |||
| val audioFileNameUk = | |||
| "${base.subjectId}_${base.coursePackId}_${base.courseId}_${wordId}_${AppConstants.SOUND_TYPE_UK}" | |||
| val file = File(parentPath, audioFileNameUk) | |||
| FileUtil.writeBytesToFile(file, it) | |||
| file.path | |||
| } ?: audio_us?.let { | |||
| val audioFileNameUk = | |||
| "${base.subjectId}_${base.coursePackId}_${base.courseId}_${wordId}_${AppConstants.SOUND_TYPE_UK}" | |||
| val file = File(parentPath, audioFileNameUk) | |||
| FileUtil.writeBytesToFile(file, it) | |||
| file.path | |||
| } | |||
| if (soundWay == AppConstants.SOUND_TYPE_UK) audio_uk_file_path else audio_us_file_path | |||
| } | |||
| //只有一种中文发音 | |||
| AppConstants.COURSE_TYPE_CHINESE_LITERACY, AppConstants.COURSE_TYPE_CHINESE_PINYIN -> { | |||
| var audio_cn_file_path : String? = null | |||
| val sql = "SELECT audio FROM res WHERE word_id = $wordId" | |||
| open(base) | |||
| mDataBase?.rawQuery(sql, null)?.let { | |||
| while (it.moveToNext()) { | |||
| //写入文件 | |||
| audio_cn_file_path = it.getBlobOrNull(0)?.let { | |||
| val audioFileNameCN = | |||
| "${base.subjectId}_${base.coursePackId}_${base.courseId}_${wordId}_${AppConstants.SOUND_TYPE_CN}" | |||
| val file = File(FileUtil.getSaveDirPath(AppConfig.VOICE), audioFileNameCN) | |||
| FileUtil.writeBytesToFile(file, it) | |||
| file.path | |||
| } | |||
| } | |||
| it.close() | |||
| } | |||
| audio_cn_file_path | |||
| } | |||
| //作文知识点没有发音 | |||
| else -> null | |||
| } | |||
| } | |||
| /** 获取课时学习内容 */ | |||
| fun queryLessonLearnData( | |||
| coursePackId : Long, | |||
| courseId : Long, | |||
| chapterId : Long, | |||
| lessonId : Long, | |||
| wordIds : ArrayList<Long>, | |||
| ) { | |||
| /** | |||
| * 查询学习数据 | |||
| * 适用课程类型: 单词课程、口语词汇、口语句型语法、音标、识字、拼音 | |||
| * @param dbControlBase DbControlBase | |||
| * @param lesson Lesson | |||
| * @return List<LearnWord>? | |||
| */ | |||
| @SuppressLint("Range") | |||
| fun queryLearnDataForWord(dbcb : DbControlBase, lesson : Lesson) : @NonNull List<LearnWord> { | |||
| val result = mutableListOf<LearnWord>() | |||
| open(dbcb) | |||
| //从lesson已学位置开始获取数据 | |||
| val needLearnIds = lesson.wordIds.subList(lesson.learnedIndex + 1, lesson.wordIds.size) | |||
| val sql = | |||
| "SELECT * FROM chapter WHERE chapter_id = ${lesson.chapterId} and lesson_id = ${lesson.lessonId} AND word_id in (${ | |||
| Joiner.on(",").join(needLearnIds) | |||
| }) " + "ORDER by word_sort ASC" | |||
| mDataBase?.rawQuery(sql, null)?.run { | |||
| while (moveToNext()) { | |||
| //单词id | |||
| val word_id = getLong(getColumnIndex("word_id")) | |||
| //学习单词实体 | |||
| result.add(LearnWord(dbcb.subjectId, | |||
| dbcb.coursePackId, | |||
| dbcb.courseId, | |||
| dbcb.coursePackType, | |||
| dbcb.courseType, | |||
| lesson.chapterId, | |||
| lesson.lessonId, | |||
| word_id, | |||
| true, | |||
| lesson.lessonType).apply { | |||
| word = getString(getColumnIndex("word")) | |||
| basic_explanation = getString(getColumnIndex("basic_explaination")) | |||
| extend_explanation = getString(getColumnIndex("all_explaination")) | |||
| phrase = getString(getColumnIndex("pharse")) | |||
| example = getString(getColumnIndex("example")) | |||
| reference = getString(getColumnIndex("reference")) | |||
| when (dbcb.courseType) { | |||
| AppConstants.COURSE_TYPE_CHINESE_LITERACY -> { | |||
| literacyIspolyphone = getInt(getColumnIndex("polyphone")) > 0 | |||
| phonetic_cn = getString(getColumnIndex("phonetic")) | |||
| } | |||
| AppConstants.COURSE_TYPE_CHINESE_PINYIN -> { | |||
| phonetic_cn = getString(getColumnIndex("phonetic")) | |||
| } | |||
| else -> { | |||
| phonetic_uk = getString(getColumnIndex("phonetic_uk")) | |||
| phonetic_us = getString(getColumnIndex("phonetic_us")) | |||
| } | |||
| } | |||
| //口语句型学习有句型字段 | |||
| if (lessonType == AppConstants.LESSON_TYPE_SENTENCE) { | |||
| pattern = getString(getColumnIndex("pattern")) | |||
| } | |||
| }) | |||
| } | |||
| close() | |||
| } | |||
| return result | |||
| } | |||
| /** 获取课程今日需要复习的数据 */ | |||
| fun queryCourseReviewDatas( | |||
| coursePackId : Long, | |||
| courseId : Long, | |||
| wordIds : ArrayList<Long>, | |||
| ) { | |||
| /**获取图片*/ | |||
| fun queryPhoto(dbControlBase : DbControlBase, wordId : Long) : ByteArray? { | |||
| open(dbControlBase) | |||
| var result : ByteArray? = null | |||
| val sql = "SELECT photo FROM res WHERE word_id = $wordId" | |||
| mDataBase?.rawQuery(sql,null)?.apply { | |||
| while (moveToNext()){ | |||
| result = getBlob(0) | |||
| } | |||
| close() | |||
| } | |||
| return result | |||
| } | |||
| @@ -8,5 +8,6 @@ package com.xkl.cdl.data.manager.db | |||
| data class DbControlBase( | |||
| val subjectId: Int = 0, | |||
| val coursePackId: Long = 0, | |||
| val coursePackType:Int = 0 , | |||
| val courseId: Long = 0, | |||
| var courseType: Int = 0) | |||
| @@ -75,7 +75,7 @@ class DbCoursePackManager { | |||
| coursePack.inCoursePackPosition = englishCoursePack.size | |||
| englishCoursePack.add(coursePack) | |||
| } else { | |||
| coursePack.inCoursePackPosition = englishCoursePack.size | |||
| coursePack.inCoursePackPosition = chineseCoursePack.size | |||
| chineseCoursePack.add(coursePack) | |||
| } | |||
| } | |||
| @@ -1,8 +1,17 @@ | |||
| package com.xkl.cdl.data.repository | |||
| import android.graphics.Bitmap | |||
| import android.util.LruCache | |||
| import java.security.Key | |||
| import androidx.lifecycle.MutableLiveData | |||
| import com.suliang.common.AppConfig | |||
| import com.suliang.common.eventbus.NonStickyMutableLiveData | |||
| import com.suliang.common.extension.diskIo2Main | |||
| import com.suliang.common.util.file.FileUtil | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.manager.db.DBCourseManager | |||
| import com.xkl.cdl.data.manager.db.DbControlBase | |||
| import io.reactivex.rxjava3.core.Observable | |||
| import java.io.File | |||
| import java.util.* | |||
| /** | |||
| * author suliang | |||
| @@ -10,28 +19,79 @@ import java.security.Key | |||
| * Describe: 音频缓存获取规则 | |||
| */ | |||
| object AudioCache { | |||
| var lruCache : LruCache<String, ByteArray> | |||
| init { | |||
| lruCache = object : LruCache<String,ByteArray>((Runtime.getRuntime().maxMemory()/16).toInt()){ | |||
| // key : 发音文件名称 value: 发音文件地址 | |||
| private var lruCache : LruCache<String, String> = | |||
| object : LruCache<String, String>((Runtime.getRuntime().maxMemory() / 16).toInt()) { | |||
| //告知每个对象的存储大小 | |||
| override fun sizeOf(key: String?, value: ByteArray?): Int { | |||
| if (value == null){ | |||
| return 0 | |||
| override fun sizeOf(key : String?, value : String?) : Int { | |||
| return value?.toByteArray()?.size ?: 0 | |||
| } | |||
| } | |||
| /** 通知查到的录音 */ | |||
| var audioLiveData = NonStickyMutableLiveData<String?>() | |||
| // fun initAudioLiveData() : MutableLiveData<String?>{ | |||
| // audioLiveData = NonStickyMutableLiveData<String?>() | |||
| // return audioLiveData | |||
| // } | |||
| /** | |||
| * 获取对应音频 | |||
| * 1缓存 2文件 3数据库 | |||
| * @return String 文件路径 | |||
| */ | |||
| fun get(dbControlBase : DbControlBase, wordId : Long, soundWay : Int) { | |||
| //父文件路劲 | |||
| val parentPath = FileUtil.getSaveDirPath(AppConfig.VOICE) | |||
| //先查对应的文件路径 : 项目id_课程包id_课程id_单词id_发音方式 | |||
| val defaultKey = "${dbControlBase.subjectId}_${dbControlBase.coursePackId}_${dbControlBase.courseId}_${wordId}_$soundWay" | |||
| Observable.fromCallable { | |||
| //不为空,直接取值 | |||
| if (lruCache.get(defaultKey) == null){ | |||
| //为空 查文件 | |||
| val file = File(parentPath, defaultKey) | |||
| when { | |||
| file.exists() -> lruCache.put(defaultKey, file.path) //文件存在,则表示以前进行过数据库查询,且进行了保存文件,英美发音本地都有文件 | |||
| else -> { //文件不存在,则查询数据库进行查询,返回的为保存的文件地址 | |||
| val filePath = DBCourseManager.queryAudio(dbControlBase, wordId, soundWay) | |||
| filePath?.also { | |||
| when(soundWay){ | |||
| AppConstants.SOUND_TYPE_UK -> { | |||
| lruCache.put(defaultKey, it) //保存路径 | |||
| val audioFileNameUS = "${dbControlBase.subjectId}_${dbControlBase.coursePackId}_${dbControlBase.courseId}_${wordId}_${AppConstants.SOUND_TYPE_US}" | |||
| lruCache.put(audioFileNameUS, File(parentPath,audioFileNameUS).path) //保存路径 | |||
| } | |||
| AppConstants.SOUND_TYPE_US -> { | |||
| lruCache.put(defaultKey, it) //保存路径 | |||
| val audioFileNameUS = "${dbControlBase.subjectId}_${dbControlBase.coursePackId}_${dbControlBase.courseId}_${wordId}_${AppConstants.SOUND_TYPE_UK}" | |||
| lruCache.put(audioFileNameUS, File(parentPath,audioFileNameUS).path) //保存路径 | |||
| } | |||
| else -> lruCache.put(defaultKey, it) //保存路径 | |||
| } | |||
| } //如果查询返回路径为空,则说明,没有发音内容 | |||
| } | |||
| } | |||
| return value.size | |||
| } | |||
| return@fromCallable lruCache.get(defaultKey) ?: "" | |||
| }.compose(diskIo2Main()).subscribe { | |||
| audioLiveData.value = it | |||
| } | |||
| } | |||
| /** | |||
| * 获取对应音频 | |||
| * @param key String | |||
| * @return ByteArray? | |||
| * 口语总测获取发音文件 | |||
| * @param fileName String 发音文件名称 | |||
| * @return String 发音文件路径 | |||
| */ | |||
| fun get(key:String) : ByteArray? { | |||
| return null | |||
| fun get(fileName : String):String { | |||
| val parentPath = FileUtil.getSaveDirPath(AppConfig.VOICE) | |||
| if (lruCache[fileName] == null){ | |||
| lruCache.put(fileName,File(parentPath,fileName).path) | |||
| } | |||
| return lruCache[fileName] | |||
| } | |||
| } | |||
| @@ -0,0 +1,56 @@ | |||
| package com.xkl.cdl.data.repository | |||
| import android.util.LruCache | |||
| import androidx.lifecycle.MutableLiveData | |||
| import com.suliang.common.AppConfig | |||
| import com.suliang.common.eventbus.NonStickyMutableLiveData | |||
| import com.suliang.common.extension.diskIo2Main | |||
| import com.suliang.common.util.file.FileUtil | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.manager.db.DBCourseManager | |||
| import com.xkl.cdl.data.manager.db.DbControlBase | |||
| import io.reactivex.rxjava3.core.Observable | |||
| import java.io.File | |||
| import java.util.* | |||
| /** | |||
| * author suliang | |||
| * create 2022/4/1 9:53 | |||
| * Describe: 图片缓存获取规则 | |||
| */ | |||
| object PhotoCache { | |||
| // key : value: 发音文件地址 | |||
| private var lruCache : LruCache<String, ByteArray?> = | |||
| object : LruCache<String, ByteArray?>((Runtime.getRuntime().maxMemory() / 16).toInt()) { | |||
| //告知每个对象的存储大小 | |||
| override fun sizeOf(key : String?, value : ByteArray?) : Int { | |||
| return value?.size ?: 0 | |||
| } | |||
| } | |||
| /** 通知查到的录音 */ | |||
| var photoLiveData = NonStickyMutableLiveData<ByteArray?>() | |||
| /** | |||
| * 获取对应图片 | |||
| * 1缓存 2文件 3数据库 | |||
| * @return String 文件路径 | |||
| */ | |||
| fun get(dbControlBase : DbControlBase, wordId : Long) { | |||
| //先查对应的文件路径 : 项目id_课程包id_课程id_单词id_发音方式 | |||
| val defaultKey = "${dbControlBase.subjectId}_${dbControlBase.coursePackId}_${dbControlBase.courseId}_${wordId}" | |||
| Observable.fromCallable { | |||
| //不为空,直接取值 | |||
| if (lruCache.get(defaultKey) == null) { | |||
| val value = DBCourseManager.queryPhoto(dbControlBase, wordId) | |||
| lruCache.put(defaultKey, value) //保存路径 | |||
| } | |||
| return@fromCallable lruCache.get(defaultKey) | |||
| }.compose(diskIo2Main()).subscribe { | |||
| photoLiveData.value = it | |||
| } | |||
| } | |||
| } | |||
| @@ -6,6 +6,7 @@ import android.view.View | |||
| import androidx.core.content.ContextCompat | |||
| import com.suliang.common.AppConfig | |||
| import com.suliang.common.base.BaseDialogFragment | |||
| import com.suliang.common.extension.click | |||
| import com.suliang.common.util.DrawableUti | |||
| import com.xkl.cdl.databinding.DialogCommonBinding | |||
| @@ -51,29 +52,33 @@ class CommonDialog private constructor() : BaseDialogFragment<DialogCommonBindin | |||
| binding.tvTitle.setText(it) | |||
| binding.tvTitle.visibility = View.VISIBLE | |||
| } | |||
| contentText?.let { | |||
| contentColor?.let { | |||
| binding.tvContent.setTextColor(ContextCompat.getColor(requireContext(),it)) | |||
| binding.tvContent.visibility = View.VISIBLE | |||
| } | |||
| contentColor?.let { | |||
| contentText?.let { | |||
| binding.tvContent.setText(it) | |||
| binding.tvContent.visibility = View.VISIBLE | |||
| } | |||
| leftColor?.let { | |||
| binding.tvLeft.setTextColor(ContextCompat.getColor(requireContext(),it)) | |||
| } | |||
| leftText?.let { | |||
| binding.tvLeft.setText(it) | |||
| onCommonDialogButtonClickListener(this@CommonDialog,false) | |||
| binding.tvLeft.click { | |||
| onCommonDialogButtonClickListener(this@CommonDialog,false) | |||
| } | |||
| }?:let { | |||
| binding.tvLeft.visibility = View.GONE | |||
| binding.vSpace.visibility = View.GONE | |||
| } | |||
| rightText?.let { | |||
| rightColor?.let { | |||
| binding.tvRight.setTextColor(ContextCompat.getColor(requireContext(),it)) | |||
| onCommonDialogButtonClickListener(this@CommonDialog,true) | |||
| } | |||
| rightColor?.let { | |||
| rightText?.let { | |||
| binding.tvRight.setText(it) | |||
| binding.tvRight.click { | |||
| onCommonDialogButtonClickListener(this@CommonDialog,true) | |||
| } | |||
| } | |||
| imgFlag?.let { | |||
| @@ -19,10 +19,9 @@ import com.xkl.cdl.databinding.DialogLessonLearnBinding | |||
| * create 2022/4/2 15:47 | |||
| * Describe: 学习的通用弹窗 | |||
| */ | |||
| class LearnDialog private constructor(): BaseDialogFragment<DialogLessonLearnBinding>() { | |||
| class LearnDialog private constructor() : BaseDialogFragment<DialogLessonLearnBinding>() { | |||
| companion object { | |||
| fun newInstance(params : LearnDialogBean) : LearnDialog { | |||
| val args = Bundle() | |||
| args.putParcelable(AppConfig.INTENT_1, params) | |||
| @@ -49,13 +48,23 @@ class LearnDialog private constructor(): BaseDialogFragment<DialogLessonLearnBin | |||
| override fun initFragment() { | |||
| when (learnDialogBean.dialogType) { | |||
| AppConstants.DIALOG_TYPE_EXAM -> when (learnDialogBean.examType) { | |||
| //测试开始弹窗 | |||
| AppConstants.DIALOG_TYPE_EXAM_START -> when(learnDialogBean.examType){ | |||
| //课时学前测试开始弹窗 | |||
| AppConstants.TEST_TYPE_BEFORE -> initLessonBeforeTestStart() | |||
| } | |||
| //测试结束弹窗 | |||
| AppConstants.DIALOG_TYPE_EXAM_OVER -> when (learnDialogBean.examType) { | |||
| //学前总测结束弹窗 | |||
| AppConstants.TEST_TYPE_BEFORE_TOTAL -> initCourseBeforeTotalTestOver() | |||
| //课时学前测试结束弹窗 | |||
| AppConstants.TEST_TYPE_BEFORE-> initLessonBeforeTestOver() | |||
| } | |||
| AppConstants.DIALOG_TYPE_LEARN -> { | |||
| //学习结束弹窗 | |||
| AppConstants.DIALOG_TYPE_LEARN_OVER -> { | |||
| } | |||
| } | |||
| } | |||
| override fun onStart() { | |||
| @@ -76,27 +85,23 @@ class LearnDialog private constructor(): BaseDialogFragment<DialogLessonLearnBin | |||
| when { | |||
| learnDialogBean.score < AppConstants.TEST_SCORE_LEVEL_1 -> { | |||
| binding.imgIv.setImageResource(R.mipmap.test_score_level_1) | |||
| binding.tvScore.setTextColor(ContextCompat.getColor(requireContext(),R.color.gray_2)) | |||
| binding.tvScore.setTextColor(ContextCompat.getColor(requireContext(), R.color.gray_2)) | |||
| } | |||
| learnDialogBean.score < AppConstants.TEST_SCORE_LEVEL_2 -> { | |||
| binding.imgIv.setImageResource(R.mipmap.test_score_level_2) | |||
| binding.tvScore.setTextColor(ContextCompat.getColor(requireContext(),R.color.red_2)) | |||
| binding.tvScore.setTextColor(ContextCompat.getColor(requireContext(), R.color.red_2)) | |||
| } | |||
| else -> { | |||
| binding.imgIv.setImageResource(R.mipmap.test_score_level_3) | |||
| binding.tvScore.setTextColor(ContextCompat.getColor(requireContext(),R.color.red_1)) | |||
| binding.tvScore.setTextColor(ContextCompat.getColor(requireContext(), R.color.red_1)) | |||
| } | |||
| } | |||
| //分数 | |||
| binding.tvScore.run { | |||
| visibility = View.VISIBLE | |||
| text = "${learnDialogBean.score}分" | |||
| } | |||
| //本次测试成绩显示 | |||
| binding.tvTip.visibility = View.VISIBLE | |||
| binding.tvScore.text = "${learnDialogBean.score}分" | |||
| } | |||
| /** 设置成绩数量 */ | |||
| private fun initNumber(){ | |||
| private fun initNumber() { | |||
| binding.incStatisticsNumber.run { | |||
| tvCorrectNumber.text = "${learnDialogBean.correctNumber}" | |||
| tvErrorNumber.text = "${learnDialogBean.errorNumber}" | |||
| @@ -106,97 +111,108 @@ class LearnDialog private constructor(): BaseDialogFragment<DialogLessonLearnBin | |||
| /** 学前总测试 结束 */ | |||
| private fun initCourseBeforeTotalTestOver() { | |||
| binding.groupTotalTest.visibility = View.VISIBLE | |||
| binding.run { | |||
| tvScore.visibility = View.VISIBLE | |||
| tvTip.visibility = View.VISIBLE | |||
| tvTip1.visibility = View.VISIBLE | |||
| tvTitle.visibility = View.VISIBLE | |||
| incStatisticsNumber.root.visibility = View.VISIBLE | |||
| tvTitle.text = "恭喜你,完成了学前总测试!" | |||
| tvTip1.text = "学考乐已为您智能生成了个性化学习计划" | |||
| tvRight.text = resources.getString(R.string.start_learn) | |||
| tvRight.click { | |||
| onDialogListener(AppConstants.ACTION_COURSE_TEST_START_LEARN, this@LearnDialog) | |||
| } | |||
| } | |||
| initScore() | |||
| binding.tvTitle.text = "恭喜你,完成了学前总测试!" | |||
| binding.tvTip1.text = "学考乐已为您智能生成了个性化学习计划" | |||
| initNumber() | |||
| binding.tvRight.text = resources.getString(R.string.start_learn) | |||
| binding.tvRight.click { | |||
| onDialogListener(AppConstants.ACTION_COURSE_TEST_START_LEARN, this) | |||
| } | |||
| } | |||
| fun initLessonBeforeTest() { | |||
| binding.tvTitle.text = "课时学前测试" | |||
| binding.tvLessonName.text = "章节课时名称" | |||
| binding.tvCountTime.text = "测试题目时间" | |||
| binding.tvRight.text = "开始测试" | |||
| binding.tvLeft.visibility = View.VISIBLE | |||
| binding.tvLeft.text = "随便设置" | |||
| binding.tvRight.click { | |||
| LogUtil.e("Dialog -- > show()") | |||
| } | |||
| binding.tvLeft.click { | |||
| LogUtil.e("Dialog -- > hide()") | |||
| /** | |||
| * 课时学前测试开始开始前提示 | |||
| */ | |||
| private fun initLessonBeforeTestStart() { | |||
| binding.run { | |||
| tvTitle.visibility = View.VISIBLE | |||
| tvLessonName.visibility = View.VISIBLE | |||
| tvCountTime.visibility = View.VISIBLE | |||
| tvTitle.text = "课时学前测试" | |||
| tvLessonName.text = learnDialogBean.chapter_lesson_name | |||
| tvCountTime.text = learnDialogBean.showTimeCount | |||
| tvRight.text = "开始测试" | |||
| imgIv.setImageResource(R.mipmap.girl_1) | |||
| //开始测试,进入课时学前测试界面 | |||
| binding.tvRight.click { | |||
| onDialogListener(AppConstants.DIALOG_START_TEST, this@LearnDialog) | |||
| } | |||
| // tvLeft.visibility = View.VISIBLE | |||
| // vSplit.visibility = View.VISIBLE | |||
| // tvLeft.text = "跳过测试" | |||
| //跳过测试,直接进入学习界面 | |||
| // binding.tvLeft.click { | |||
| // onDialogListener(AppConstants.DIALOG_START_LEARN, this@LearnDialog) | |||
| // } | |||
| } | |||
| } | |||
| fun initLessonBeforeTestOver() { | |||
| binding.tvScore.run { | |||
| visibility = View.VISIBLE | |||
| text = "100分" | |||
| /** | |||
| * 课时学前测试结束 | |||
| */ | |||
| private fun initLessonBeforeTestOver() { | |||
| binding.run { | |||
| tvScore.visibility = View.VISIBLE | |||
| tvTip.visibility = View.VISIBLE | |||
| tvTitle.visibility = View.VISIBLE | |||
| tvTip1.visibility = View.VISIBLE | |||
| incStatisticsNumber.root.visibility = View.VISIBLE | |||
| tvTitle.text = "恭喜你,完成了课时学前测试" | |||
| tvTip1.setText(resources.getString(R.string.test_before_test_over_tip)) | |||
| tvRight.setText(resources.getString(R.string.start_learn)) | |||
| } | |||
| binding.tvTip.visibility = View.VISIBLE | |||
| binding.tvTitle.text = "恭喜你,完成了课时学前测试" | |||
| binding.tvLessonName.visibility = View.GONE | |||
| binding.tvCountTime.visibility = View.GONE | |||
| binding.tvTip1.visibility = View.VISIBLE | |||
| binding.tvTip1.setText(resources.getString(R.string.test_before_test_over_tip)) | |||
| binding.incStatisticsNumber.root.visibility = View.VISIBLE | |||
| binding.tvLeft.visibility = View.GONE | |||
| binding.tvRight.setText(resources.getString(R.string.start_learn)) | |||
| initScore() | |||
| initNumber() | |||
| binding.tvRight.click { | |||
| // TODO: 2022/4/2 开始学习 | |||
| //开始学习 | |||
| onDialogListener(AppConstants.DIALOG_START_LEARN,this) | |||
| } | |||
| } | |||
| fun initLessonAfterTestOver() { | |||
| //未通过、通过和通过已通过,均为一致,只是显示的状态不同 | |||
| val score = if (1 + 1 == 2) 100 else 88 | |||
| when { | |||
| score < AppConstants.TEST_SCORE_LEVEL_1 -> { //小于80 | |||
| binding.imgIv.setImageResource(R.mipmap.test_score_level_1) | |||
| binding.tvScore.setTextColor(ContextCompat.getColor(requireContext(), R.color.gray_2)) | |||
| } | |||
| score >= AppConstants.TEST_SCORE_LEVEL_2 -> { //大于90 | |||
| binding.imgIv.setImageResource(R.mipmap.test_score_level_2) | |||
| binding.tvScore.setTextColor(ContextCompat.getColor(requireContext(), R.color.red_1)) | |||
| } | |||
| else -> { // 大于等于80 且小于90 | |||
| binding.imgIv.setImageResource(R.mipmap.test_score_level_3) | |||
| binding.tvScore.setTextColor(ContextCompat.getColor(requireContext(), R.color.red_2)) | |||
| } | |||
| /** | |||
| * 课时测试结束 | |||
| */ | |||
| private fun initLessonAfterTestOver(){ | |||
| initNumber() | |||
| initScore() | |||
| binding.run { | |||
| tvScore.visibility = View.VISIBLE | |||
| tvTip.visibility = View.VISIBLE | |||
| tvTitle.visibility = View.VISIBLE | |||
| tvTip1.visibility = View.VISIBLE | |||
| incStatisticsNumber.root.visibility = View.VISIBLE | |||
| tvTop.visibility = View.VISIBLE | |||
| tvLeft.visibility = View.VISIBLE | |||
| vSplit.visibility = View.VISIBLE | |||
| tvTitle.text = "恭喜你,完成了课时学后测试" | |||
| tvTop1.text = "重新学习" | |||
| tvLeft.text = "再测一次" | |||
| tvRight.text = "下一课时" | |||
| // TODO: 2022/4/21 小游戏练习先搁置 | |||
| // tvTop1.visibility = View.VISIBLE | |||
| // tvTop.text = "小游戏练习" | |||
| } | |||
| binding.tvScore.run { | |||
| visibility = View.VISIBLE | |||
| text = "${score}分" | |||
| binding.tvTop.click { | |||
| onDialogListener(AppConstants.DIALOG_LESSON_AFTER_TEST_RELEARN,this) | |||
| } | |||
| binding.tvLeft.click { | |||
| onDialogListener(AppConstants.DIALOG_LESSON_AFTER_TEST_AGAIN,this) | |||
| } | |||
| binding.tvTip.visibility = View.VISIBLE | |||
| binding.tvTitle.text = "恭喜你,完成了课时学前测试" | |||
| binding.tvLessonName.visibility = View.GONE | |||
| binding.tvCountTime.visibility = View.GONE | |||
| binding.incStatisticsNumber.root.visibility = View.VISIBLE | |||
| // binding.tvTip1.visibility = View.VISIBLE | |||
| // binding.tvTip1.setText(resources.getString(R.string.test_before_test_over_tip)) | |||
| binding.tvRight.setText(resources.getString(R.string.start_learn)) | |||
| binding.tvRight.click { | |||
| // TODO: 2022/4/2 开始学习 | |||
| onDialogListener(AppConstants.DIALOG_LESSON_AFTER_TEST_NEXT,this) | |||
| } | |||
| } | |||
| } | |||
| @@ -5,6 +5,7 @@ import android.os.Looper | |||
| import androidx.lifecycle.LifecycleOwner | |||
| import androidx.lifecycle.MutableLiveData | |||
| import com.suliang.common.base.viewmodel.BaseViewModel | |||
| import com.suliang.common.util.LogUtil | |||
| import java.util.* | |||
| import kotlin.concurrent.timerTask | |||
| @@ -25,26 +26,21 @@ abstract class LearnBaseViewModel : BaseViewModel() { | |||
| private var timeTask : TimerTask? = null | |||
| //是否进行计时,默认计时,为false不计时,更改为false的条件时,出现弹窗或者当前页不在前台 | |||
| var countingEnable = true | |||
| // var countingEnable = true | |||
| /** 开始计时 */ | |||
| fun startTotalCounting(){ | |||
| timeTask = timerTask { | |||
| if (countingEnable) { | |||
| totalUseTime.postValue(totalUseTime.value?.plus(1000)) | |||
| } | |||
| }.apply { | |||
| if (!isAllOver) { | |||
| totalTimer.schedule(this, 1000, 1000) | |||
| } | |||
| totalUseTime.postValue(totalUseTime.value?.plus(200)) | |||
| } | |||
| totalTimer.schedule(timeTask, 200, 200) | |||
| LogUtil.e("开始总计时") | |||
| } | |||
| /** 停止计时 */ | |||
| fun stopTotalCountTing(){ | |||
| totalTimer.cancel() | |||
| LogUtil.e("停止总计时") | |||
| timeTask?.cancel() | |||
| timeTask = null | |||
| } | |||
| } | |||
| @@ -9,14 +9,18 @@ import android.view.View | |||
| import android.widget.TextView | |||
| import androidx.core.content.ContextCompat | |||
| import androidx.lifecycle.ViewModelProvider | |||
| import androidx.recyclerview.widget.GridLayoutManager | |||
| import androidx.recyclerview.widget.SimpleItemAnimator | |||
| import androidx.recyclerview.widget.StaggeredGridLayoutManager | |||
| import com.airbnb.lottie.LottieAnimationView | |||
| import com.suliang.common.base.activity.BaseActivityVM | |||
| import com.suliang.common.eventbus.LiveDataBus | |||
| import com.suliang.common.extension.click | |||
| import com.suliang.common.util.DateUtil | |||
| import com.suliang.common.util.DrawableUti | |||
| import com.suliang.common.util.LogUtil | |||
| import com.suliang.common.util.media.EMediaState | |||
| import com.suliang.common.util.media.IMPListener | |||
| import com.suliang.common.util.media.MPManager | |||
| import com.xkl.cdl.R | |||
| import com.xkl.cdl.adapter.AdapterSpell | |||
| import com.xkl.cdl.adapter.itemdecoration.SpellItemDecoration | |||
| @@ -26,6 +30,7 @@ import com.xkl.cdl.data.bean.course.ExamBean | |||
| import com.xkl.cdl.data.event.LearnEventData | |||
| import com.xkl.cdl.data.manager.CourseManager | |||
| import com.xkl.cdl.data.manager.UserInfoManager | |||
| import com.xkl.cdl.data.repository.AudioCache | |||
| import com.xkl.cdl.databinding.* | |||
| import com.xkl.cdl.dialog.CommonDialog | |||
| import com.xkl.cdl.dialog.CommonDialogBean | |||
| @@ -74,6 +79,12 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| resources.getString(R.string.pause) -> { //暂停点击 | |||
| vm.pauseToNext() | |||
| setText(R.string.continue_) | |||
| // //如果是学前学后总测试的口语,点击暂停按钮后,需要将选项文字的点击事件恢复,用以点击发音 | |||
| // if (vm.intentData.courseType == AppConstants.COURSE_TYPE_ENGLISH_SPOKEN && (vm.intentData.examType == AppConstants.TEST_TYPE_BEFORE_TOTAL || vm.intentData.examType == AppConstants.TEST_TYPE_AFTER_TOTAL)) { | |||
| // vm.optionList.forEach { optionBinding -> | |||
| // optionBinding.ivOption.click { optionBinding.ivOption.performClick() } | |||
| // } | |||
| // } | |||
| } | |||
| resources.getString(R.string.continue_) -> { | |||
| vm.continueToNext() | |||
| @@ -82,6 +93,57 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** 查询到播放数据过来 */ | |||
| AudioCache.audioLiveData.observe(this) { | |||
| if (it == null) { | |||
| //发音文件为空 | |||
| showToast("未找到发音文件") | |||
| } else { | |||
| MPManager.play(it, listener = impListener) | |||
| } | |||
| } | |||
| } | |||
| //播放监听 | |||
| private val impListener = object : IMPListener { | |||
| override fun onMpState(state : EMediaState) { | |||
| when (vm.intentData.courseType) { | |||
| AppConstants.COURSE_TYPE_ENGLISH_DISCERN, AppConstants.COURSE_TYPE_ENGLISH_SOUNDMARK, AppConstants.COURSE_TYPE_CHINESE_LITERACY, AppConstants.COURSE_TYPE_CHINESE_PINYIN, //不需要监听,只播放一次 | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL, // 拼写不需要监听,第一次不播放 | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPOKEN -> { | |||
| } | |||
| AppConstants.COURSE_TYPE_ENGLISH_VOICE -> { //辨音需要监听是否需要重复播放 | |||
| when (state) { | |||
| EMediaState.RUNNING -> if (wordChooseBinding.ivVoice.visibility == View.VISIBLE) { | |||
| wordChooseBinding.ivVoice.playAnimation() | |||
| } | |||
| EMediaState.ERROR -> showToast("播放异常") | |||
| EMediaState.COMPLETE -> if (wordChooseBinding.ivVoice.visibility == View.VISIBLE) { | |||
| wordChooseBinding.ivVoice.postDelayed({ | |||
| LogUtil.e("------开始重复播放----------------------------------------") | |||
| wordChooseBinding.ivVoice.performClick() | |||
| }, 700) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** 新单词来了的发音事件 */ | |||
| private fun initNewWordRead(it : ExamBean) { | |||
| when (vm.intentData.courseType) { | |||
| //拼写初始不发音,作文不发音 | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL, AppConstants.COURSE_TYPE_CHINESE_COMPOSITION -> { | |||
| } | |||
| //英语认读,英语音标,英语口语,语文识字,语文拼音 出现即发音一次 | |||
| AppConstants.COURSE_TYPE_ENGLISH_DISCERN, AppConstants.COURSE_TYPE_ENGLISH_SOUNDMARK, AppConstants.COURSE_TYPE_ENGLISH_SPOKEN, AppConstants.COURSE_TYPE_CHINESE_LITERACY, AppConstants.COURSE_TYPE_CHINESE_PINYIN, | |||
| // 英语辨音 重复播放 | |||
| AppConstants.COURSE_TYPE_ENGLISH_VOICE -> { | |||
| wordChooseBinding.ivVoice.performClick() | |||
| } | |||
| } | |||
| } | |||
| @@ -95,14 +157,24 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| tvTitle.text = vm.intentData.showTitle | |||
| //题目进度 | |||
| binding.incLearnTitle.tvNumProgress.text = "${vm.currentIndex + 1}/${vm.testData.size}" | |||
| //默认发音 | |||
| vm.defaultSoundWay = UserInfoManager.getDefaultSoundWay() | |||
| voiceSwitch.setSoundWay(vm.defaultSoundWay) | |||
| voiceSwitch.soundWayChange.observe(this@LearnExamActivity) { | |||
| vm.defaultSoundWay = it | |||
| UserInfoManager.putDefaultSoundWay(it) | |||
| if (this@LearnExamActivity::spellAdapter.isInitialized) { | |||
| spellAdapter.defaultSoundWay = it | |||
| //发音方式 | |||
| when (vm.intentData.subjectId) { | |||
| AppConstants.SUBJECT_CHINESE -> { | |||
| vm.defaultSoundWay = AppConstants.SOUND_TYPE_CN | |||
| voiceSwitch.visibility = View.GONE | |||
| } | |||
| else -> { | |||
| //默认发音 | |||
| vm.defaultSoundWay = UserInfoManager.getDefaultSoundWay() | |||
| voiceSwitch.setSoundWay(vm.defaultSoundWay) | |||
| voiceSwitch.soundWayChange.observe(this@LearnExamActivity) { | |||
| vm.defaultSoundWay = it | |||
| UserInfoManager.putDefaultSoundWay(it) | |||
| if (this@LearnExamActivity::spellAdapter.isInitialized) { | |||
| spellAdapter.defaultSoundWay = it | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -134,8 +206,9 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL -> { | |||
| spellBinding = IncExamSpellContentBinding.inflate(layoutInflater, binding.containerLayout, true) | |||
| spellBinding.spellRecyclerView.run { | |||
| layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.HORIZONTAL) | |||
| layoutManager = GridLayoutManager(this@LearnExamActivity, 2, GridLayoutManager.HORIZONTAL, false) | |||
| addItemDecoration(SpellItemDecoration()) | |||
| setHasFixedSize(true) | |||
| (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false | |||
| spellAdapter = AdapterSpell().apply { | |||
| onItemSpellingClickListener = itemSpellingClick | |||
| @@ -144,7 +217,7 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| adapter = spellAdapter | |||
| } | |||
| //初始化事件 | |||
| initChooseListener() | |||
| initChooseQuestionListener() | |||
| } | |||
| else -> { | |||
| wordChooseBinding = IncExamWordChooseContentBinding.inflate(layoutInflater, binding.containerLayout, true) | |||
| @@ -162,7 +235,7 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| tvLable.text = "D" | |||
| }) | |||
| //初始化事件 | |||
| initChooseListener() | |||
| initChooseQuestionListener() | |||
| //正常、正确、错误 文本和背景颜色 | |||
| vm.normalTextColor = ContextCompat.getColor(this, R.color.main_text_color) | |||
| vm.correctTextColor = ContextCompat.getColor(this, R.color.green_1) | |||
| @@ -180,23 +253,19 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| } | |||
| /** 初始四选一的点击事件 只处理问题和单词的事件,选项事件,每个新的单词开始时设置,答题后再取消设置 | |||
| * 拼写,释义、单词都不发音,只有拼写和拼写完成发音 | |||
| * 拼写,释义、单词都不发音,只有拼写在拼写完成发音 | |||
| * 作文知识点测试,没有发音 | |||
| * 辨音、口语单词与音频均可发音, | |||
| * 其他单词可以发音 | |||
| * */ | |||
| private fun initChooseListener() { | |||
| private fun initChooseQuestionListener() { | |||
| when (vm.intentData.courseType) { | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL, AppConstants.COURSE_TYPE_CHINESE_COMPOSITION -> { | |||
| } | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPOKEN, AppConstants.COURSE_TYPE_ENGLISH_VOICE -> { | |||
| else -> { | |||
| wordChooseBinding.tvWord.setOnClickListener(ivVoiceClick) | |||
| wordChooseBinding.ivVoice.setOnClickListener(ivVoiceClick) | |||
| } | |||
| else -> { //四选一题目可以发音 | |||
| wordChooseBinding.tvWord.setOnClickListener(ivVoiceClick) | |||
| } | |||
| } | |||
| } | |||
| @@ -210,7 +279,7 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| override fun loadData() { | |||
| //初始获取第一条数据 | |||
| vm.loadNext() | |||
| vm.loadFirst() | |||
| //监听倒计时 | |||
| vm.currentSuplusTime.observe(this) { | |||
| @@ -230,7 +299,8 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| initContentUI(it) | |||
| //设置初始事件 | |||
| initChooseContentLister(it) | |||
| vm.startCurrentCountingTime() | |||
| //发音事件 | |||
| initNewWordRead(it) | |||
| } ?: testOver() | |||
| } | |||
| @@ -263,6 +333,16 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| if (it != AppConstants.TEST_CORRECT) { | |||
| binding.tvErrorState.visibility = View.VISIBLE | |||
| } | |||
| if (isSpokenTotalTest()) { | |||
| vm.optionList.forEach { optionBinding -> | |||
| optionBinding.run { | |||
| groupVoiceChoose.visibility = View.GONE | |||
| tvOption.visibility = View.VISIBLE | |||
| } | |||
| } | |||
| } | |||
| //获取下一题 | |||
| vm.loadNext() | |||
| } | |||
| @@ -275,60 +355,25 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| //获取下一题 | |||
| vm.loadNext() | |||
| } | |||
| } | |||
| /** 四选一对option设置事件 新数据来的时候 设置option的事件 | |||
| * @param examBean 非空,则需要设置事件 为空则取消点击事件 | |||
| * */ | |||
| private fun initChooseContentLister(examBean : ExamBean?) { | |||
| when (vm.intentData.courseType) { | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPOKEN -> { | |||
| examBean?.let { | |||
| when (it.answersIsAudio) { //选项是否音频 | |||
| true -> { | |||
| // TODO: 2022/4/13 设置音频事件的路劲tag | |||
| vm.optionList.forEachIndexed { index, optionItemBinding -> | |||
| optionItemBinding.layoutChooseArea.click { vm.chooseResult(index) } | |||
| optionItemBinding.layoutLottieVoiceArea.click(ivVoiceClick) | |||
| optionItemBinding.tvOption.click(null) | |||
| optionItemBinding.tvOption.tag = "" | |||
| } | |||
| } | |||
| false -> { | |||
| vm.optionList.forEachIndexed { index, optionItemBinding -> | |||
| optionItemBinding.root.click { vm.chooseResult(index) } | |||
| optionItemBinding.tvOption.click(null) | |||
| optionItemBinding.tvOption.tag = "" | |||
| } | |||
| } | |||
| } | |||
| } ?: let { //取消事件 | |||
| vm.optionList.forEachIndexed { index, optionItemBinding -> | |||
| optionItemBinding.root.click(null) | |||
| optionItemBinding.tvOption.click(ivVoiceClick) | |||
| } | |||
| } | |||
| } | |||
| else -> { | |||
| examBean?.let { | |||
| vm.optionList.forEachIndexed { index, optionItemBinding -> | |||
| optionItemBinding.root.click { vm.chooseResult(index) } | |||
| } | |||
| } ?: let { | |||
| vm.optionList.forEachIndexed { index, optionItemBinding -> | |||
| optionItemBinding.root.click(null) | |||
| } | |||
| //当前单词倒计时结束 | |||
| vm.currentCountingTimeOver.observe(this) { | |||
| when (vm.intentData.courseType) { | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL -> { | |||
| spellAdapter.testCountingTimeOver() | |||
| } | |||
| else -> vm.chooseResult(-1) | |||
| } | |||
| } | |||
| } | |||
| private fun initContentUI(examBean : ExamBean) { | |||
| if (isSpokenTotalTest()) { | |||
| initSpokenUIData(examBean) | |||
| return | |||
| } | |||
| when (vm.intentData.courseType) { | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL -> initSpellUIData(examBean) | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPOKEN -> initSpokenUIData(examBean) | |||
| AppConstants.COURSE_TYPE_ENGLISH_VOICE -> initVoiceUIData(examBean) | |||
| else -> initChooseUIData(examBean) | |||
| } | |||
| @@ -379,6 +424,8 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| wordChooseBinding.tvWord.visibility = View.VISIBLE | |||
| wordChooseBinding.ivVoice.visibility = View.GONE | |||
| } | |||
| wordChooseBinding.progressWordCountdownTime.visibility = View.GONE | |||
| vm.optionList.forEachIndexed { index, optionBinding -> | |||
| optionBinding.run { | |||
| root.setBackgroundResource(vm.normalBg) | |||
| @@ -389,11 +436,9 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| if (examBean.answersIsAudio) { //答案为音频 | |||
| groupVoiceChoose.visibility = View.VISIBLE | |||
| tvOption.visibility = View.GONE | |||
| ivOption.visibility = View.GONE | |||
| } else { | |||
| groupVoiceChoose.visibility = View.GONE | |||
| tvOption.visibility = View.VISIBLE | |||
| ivOption.visibility = View.VISIBLE | |||
| } | |||
| } | |||
| } | |||
| @@ -407,7 +452,7 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| setTextColor(ContextCompat.getColor(this@LearnExamActivity, R.color.main_text_color)) | |||
| } | |||
| //进度 | |||
| wordChooseBinding.progressWordCountdownTime.run { | |||
| spellBinding.progressWordCountdownTime.run { | |||
| max = vm.currentMaxTime | |||
| progress = vm.currentMaxTime | |||
| } | |||
| @@ -416,6 +461,78 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| } | |||
| /** 四选一对option设置事件 新数据来的时候 设置option的事件 | |||
| * @param examBean 非空,则需要设置事件 为空则取消点击事件 | |||
| * */ | |||
| private fun initChooseContentLister(examBean : ExamBean?) { | |||
| if (isSpokenTotalTest()) { | |||
| examBean?.let { | |||
| when { | |||
| it.answersIsAudio -> { //选项是否音频 | |||
| vm.optionList.forEachIndexed { index, optionItemBinding -> | |||
| optionItemBinding.run { | |||
| root.click(null) | |||
| layoutChooseArea.click { vm.chooseResult(index) } | |||
| ivOption.click(ivVoiceClick) | |||
| // TODO: 2022/4/20 这个的tag为该条音频的发音音频源 | |||
| ivOption.tag = tvOption.text | |||
| layoutLottieVoiceArea.click { ivOption.performClick() } | |||
| tvOption.click(null) | |||
| } | |||
| } | |||
| } | |||
| else -> { //答案选项非音频源 | |||
| vm.optionList.forEachIndexed { index, optionItemBinding -> | |||
| optionItemBinding.run { | |||
| root.click { vm.chooseResult(index) } | |||
| ivOption.click(ivVoiceClick) | |||
| // TODO: 2022/4/20 这个的tag为该条音频的发音音频源 | |||
| ivOption.tag = tvOption.text | |||
| tvOption.click(null) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } ?: let { //取消事件 | |||
| vm.optionList.forEachIndexed { index, optionItemBinding -> | |||
| optionItemBinding.root.click(null) | |||
| optionItemBinding.tvOption.click { optionItemBinding.ivOption.performClick() } | |||
| } | |||
| } | |||
| } else { | |||
| examBean?.let { | |||
| vm.optionList.forEachIndexed { index, optionItemBinding -> | |||
| optionItemBinding.root.click { vm.chooseResult(index) } | |||
| } | |||
| } ?: let { | |||
| vm.optionList.forEachIndexed { index, optionItemBinding -> | |||
| optionItemBinding.root.click(null) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** 点击发音事件 获取发音音频,监听返回值,播放*/ | |||
| private val ivVoiceClick : (v : View) -> Unit = { view -> | |||
| vm.currentExamBean.value?.let { | |||
| //正常情况除口语,其他的都是播放的测试单词的音频 | |||
| //口语的音频,有问题,与选项 直接获取其tag获取音频 | |||
| currentPlayView?.cancelAnimation() | |||
| if (view is LottieAnimationView && view.visibility == View.VISIBLE) currentPlayView = view | |||
| if (isSpokenTotalTest()) { | |||
| //口语课程学前学后总的点击事件取tag | |||
| AudioCache.get(view.tag as String) | |||
| } else { | |||
| AudioCache.get(vm.dbBaseControl, it.word_id, vm.defaultSoundWay) | |||
| } | |||
| } | |||
| } | |||
| //是否口语总测试 | |||
| private fun isSpokenTotalTest() = | |||
| vm.intentData.courseType == AppConstants.COURSE_TYPE_ENGLISH_SPOKEN && (vm.intentData.examType == AppConstants.TEST_TYPE_BEFORE_TOTAL || vm.intentData.examType == AppConstants.TEST_TYPE_AFTER_TOTAL) | |||
| /** | |||
| * 拼写时的点击事件 | |||
| * @param selectedValue 选择的值 | |||
| @@ -433,6 +550,8 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| spellBinding.tvWord.text = correctValue | |||
| //拼写结束 | |||
| vm.spellOver(selectedValue.toString(), errorSize) | |||
| //执行当前的单词发音 延迟发音 | |||
| spellBinding.tvWord.postDelayed({ ivVoiceClick(spellBinding.tvWord) }, 200) | |||
| } | |||
| else -> { | |||
| //设置为选中的值 | |||
| @@ -443,41 +562,37 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| } | |||
| /** 点击发音事件 */ | |||
| private val ivVoiceClick : (v : View) -> Unit = { | |||
| } | |||
| override fun onBackPressed() { | |||
| onBack() | |||
| } | |||
| /** 返回: 未结束提示弹窗 */ | |||
| private fun onBack() { | |||
| vm.stopTotalCountTing() | |||
| when { | |||
| vm.isAllOver -> finish() | |||
| else -> CommonDialog.newInstance(CommonDialogBean(titleText = R.string.dialog_test_not_over, | |||
| contentText = R.string.dialog_test_not_over_tip, | |||
| leftText = R.string.quit, | |||
| rightText = R.string.quit)).apply { | |||
| onCommonDialogButtonClickListener = { dialog, isRightClick -> | |||
| if (!isRightClick){ | |||
| vm. | |||
| } | |||
| dialog.dismissAllowingStateLoss() | |||
| } | |||
| else -> { | |||
| vm.showOrDismissBackDialogForTime(true) | |||
| CommonDialog.newInstance(CommonDialogBean(titleText = R.string.dialog_test_not_over, | |||
| contentText = R.string.dialog_test_not_over_tip, | |||
| leftText = R.string.quit, | |||
| rightText = R.string.cancel)).apply { | |||
| onCommonDialogButtonClickListener = { dialog, isRightClick -> | |||
| dialog.dismissAllowingStateLoss() | |||
| when { | |||
| isRightClick -> vm.showOrDismissBackDialogForTime(false) | |||
| else -> finish() | |||
| } | |||
| } | |||
| }.show(supportFragmentManager, "common_dialog") | |||
| } | |||
| } | |||
| finish() | |||
| } | |||
| /** 测试完成 : 弹窗显示 */ | |||
| private fun testOver() { | |||
| //对话框信息实体 | |||
| val learnDialogBean = LearnDialogBean(AppConstants.DIALOG_TYPE_EXAM).apply { | |||
| val learnDialogBean = LearnDialogBean(AppConstants.DIALOG_TYPE_EXAM_OVER).apply { | |||
| examType = vm.intentData.examType | |||
| score = vm.scoreValue | |||
| correctNumber = vm.correctLiveData.value!! | |||
| @@ -485,18 +600,79 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| } | |||
| LearnDialog.newInstance(learnDialogBean).apply { | |||
| onDialogListener = { action, dialog -> | |||
| when (action) { | |||
| AppConstants.DIALOG_START_LEARN -> { //开始学习 | |||
| dialog.dismissAllowingStateLoss() | |||
| //发动动作 : 继续学习 | |||
| LiveDataBus.with<LearnEventData>(AppConstants.EVENT_COURSE).value = | |||
| LearnEventData(vm.intentData.subjectId, | |||
| vm.intentData.courseId, | |||
| AppConstants.ACTION_COURSE_TEST_START_LEARN) | |||
| finish() | |||
| when (vm.intentData.examType) { | |||
| //学前测试结束,只有一个按钮,开始学习 -> 发送消息切换到课时列表页 | |||
| AppConstants.TEST_TYPE_BEFORE_TOTAL -> when (action) { | |||
| AppConstants.DIALOG_START_LEARN -> { | |||
| dialog.dismissAllowingStateLoss() | |||
| //发送动作 : 继续学习 | |||
| LiveDataBus.with<LearnEventData>(AppConstants.EVENT_COURSE).value = | |||
| LearnEventData(vm.intentData.subjectId, | |||
| vm.intentData.courseId, | |||
| AppConstants.ACTION_COURSE_TEST_START_LEARN) | |||
| finish() | |||
| } | |||
| } | |||
| //课时学前测试,只有一个按钮 开始学习 -> 发送消息开始课时学习 | |||
| AppConstants.TEST_TYPE_BEFORE -> when (action) { | |||
| AppConstants.DIALOG_START_LEARN -> { | |||
| dialog.dismissAllowingStateLoss() | |||
| //发送动作 : 继续学习 | |||
| LiveDataBus.with<LearnEventData>(AppConstants.EVENT_COURSE).value = | |||
| LearnEventData(vm.intentData.subjectId, | |||
| vm.intentData.courseId, | |||
| AppConstants.ACTION_LESSON_BEFORE_TEST_OVER_START_LEARN).apply { | |||
| leesonPositionIndex = vm.intentData.lesson?.lessonPositionInList!! | |||
| } | |||
| finish() | |||
| } | |||
| } | |||
| //课时学后测试结束弹窗动作 | |||
| AppConstants.TEST_TYPE_AFTER -> when(action){ | |||
| //重新学习 | |||
| AppConstants.DIALOG_LESSON_AFTER_TEST_RELEARN -> { | |||
| CommonDialog.newInstance(CommonDialogBean(titleText = R.string.lesson_relearn_title, | |||
| contentText = R.string.lesson_relearn_content, | |||
| leftText = R.string.cancel, | |||
| rightText = R.string.sure)) | |||
| .apply { | |||
| onCommonDialogButtonClickListener = { relearnDialog, isRightClick -> | |||
| relearnDialog.dismissAllowingStateLoss() | |||
| if (isRightClick){ | |||
| dialog.dismissAllowingStateLoss() | |||
| //发送动作 : 重新学习 | |||
| LiveDataBus.with<LearnEventData>(AppConstants.EVENT_COURSE).value = | |||
| LearnEventData(vm.intentData.subjectId, | |||
| vm.intentData.courseId, | |||
| AppConstants.ACTION_LESSON_AFTER_TEST_RELEARN).apply { | |||
| leesonPositionIndex = vm.intentData.lesson?.lessonPositionInList!! | |||
| } | |||
| } | |||
| } | |||
| }.show(childFragmentManager,"lesson_relearn_tip") | |||
| } | |||
| //再测一次 | |||
| AppConstants.DIALOG_LESSON_AFTER_TEST_AGAIN -> { | |||
| //发送动作 : 继续学习 | |||
| LiveDataBus.with<LearnEventData>(AppConstants.EVENT_COURSE).value = | |||
| LearnEventData(vm.intentData.subjectId, | |||
| vm.intentData.courseId, | |||
| AppConstants.ACTION_LESSON_AFTER_TEST_AGAIN).apply { | |||
| leesonPositionIndex = vm.intentData.lesson?.lessonPositionInList!! | |||
| } | |||
| } | |||
| //下一步 | |||
| AppConstants.DIALOG_LESSON_AFTER_TEST_NEXT -> { | |||
| //发送动作 : 继续学习 | |||
| LiveDataBus.with<LearnEventData>(AppConstants.EVENT_COURSE).value = | |||
| LearnEventData(vm.intentData.subjectId, | |||
| vm.intentData.courseId, | |||
| AppConstants.ACTION_LESSON_AFTER_TEST_NEXT).apply { | |||
| leesonPositionIndex = vm.intentData.lesson?.lessonPositionInList!! | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }.show(supportFragmentManager, javaClass.name) | |||
| } | |||
| @@ -1,12 +0,0 @@ | |||
| package com.xkl.cdl.module.learn | |||
| import androidx.appcompat.app.AppCompatActivity | |||
| import android.os.Bundle | |||
| import com.xkl.cdl.R | |||
| class LearnExamSpellActivity : AppCompatActivity() { | |||
| override fun onCreate(savedInstanceState: Bundle?) { | |||
| super.onCreate(savedInstanceState) | |||
| setContentView(R.layout.activity_exam_learn_spell) | |||
| } | |||
| } | |||
| @@ -4,6 +4,7 @@ import android.graphics.drawable.Drawable | |||
| import androidx.lifecycle.LifecycleOwner | |||
| import androidx.lifecycle.MutableLiveData | |||
| import androidx.lifecycle.viewModelScope | |||
| import com.googlecode.protobuf.format.JsonFormat | |||
| import com.suliang.common.eventbus.LiveDataBus | |||
| import com.suliang.common.extension.createRandomNewChar | |||
| import com.suliang.common.extension.diskIo2Main | |||
| @@ -15,6 +16,7 @@ import com.xkl.cdl.data.bean.SpellItemBean | |||
| import com.xkl.cdl.data.bean.course.ExamBean | |||
| import com.xkl.cdl.data.bean.intentdata.ExamData | |||
| import com.xkl.cdl.data.event.LearnEventData | |||
| import com.xkl.cdl.data.manager.db.DbControlBase | |||
| import com.xkl.cdl.databinding.IncludTestOptionItemBinding | |||
| import io.reactivex.rxjava3.core.Observable | |||
| import kotlinx.coroutines.delay | |||
| @@ -30,7 +32,7 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| //传递过来的测试数据 | |||
| val intentData : ExamData = DataTransferHolder.instance.getData() | |||
| val dbBaseControl : DbControlBase = DbControlBase(intentData.subjectId,intentData.coursePackId,intentData.coursePackType,intentData.courseId,intentData.courseType) | |||
| //测试题数据 | |||
| val testData : List<ExamBean> by lazy { | |||
| intentData.testData!! | |||
| @@ -110,117 +112,109 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| //错误集合 | |||
| private var mLearnEntities : MutableList<LearnEntity> = mutableListOf() | |||
| //最总上传对象 | |||
| private val record = Record.newBuilder() | |||
| //学前总测试与章节前测试时新的学习错误集合 | |||
| var newErrorMap : HashMap<String, Boolean> = hashMapOf() | |||
| //标记是否获取下一题 | |||
| var isGetNextIng = false | |||
| private var isGetNextIng = false | |||
| //标记是否为当前错误,暂停获取下一题 | |||
| var isErrorPauseToNext = false | |||
| //标记当前是否倒计时中 | |||
| var isCurrentCounting = false | |||
| private var isErrorPauseToNext = false | |||
| //标记当前是否是否返回后台 | |||
| private var isInBackground = false | |||
| //标记是否显示返回弹窗 : 停止计时 | |||
| private var isShowBackDialog = false | |||
| //倒计时结束 | |||
| val currentCountingTimeOver = MutableLiveData<Boolean>() | |||
| /** 修改获取下一题的标记为true,则在onResume时,自动获取第一题 */ | |||
| fun loadFirst(){ | |||
| isGetNextIng = true | |||
| } | |||
| /** 获取下一题数据 */ | |||
| fun loadNext() { | |||
| LogUtil.e("获取下一题") | |||
| isGetNextIng = true | |||
| mHandler.postDelayed(toNextRunable, toNextTime) | |||
| } | |||
| /** 停止获取下一题 */ | |||
| fun pauseToNext() { | |||
| isErrorPauseToNext = true | |||
| mHandler.removeCallbacks(toNextRunable) | |||
| } | |||
| /** 继续获取下一题 */ | |||
| fun continueToNext() { | |||
| isErrorPauseToNext = false | |||
| isGetNextIng = true | |||
| mHandler.post(toNextRunable) | |||
| } | |||
| /** | |||
| * 显示或关闭的返回弹窗: 影响总计时和当前的一些计时 | |||
| * 显示或关闭的返回弹窗: 不影响总计时,只影响当前的下一题计时或者倒计时 | |||
| * 显示返回弹窗的时机,只会在测试当种,则是结束后则不会再显示此弹窗 | |||
| * @param isShow Boolean | |||
| */ | |||
| fun showOrDissmissBackDialogForTime(isShow: Boolean){ | |||
| fun showOrDismissBackDialogForTime(isShow : Boolean) { | |||
| isShowBackDialog = isShow | |||
| when{ | |||
| isShow -> { //显示返回弹窗 | |||
| //关闭总计时 | |||
| stopTotalCountTing() | |||
| //停止单题的倒计时 | |||
| mHandler.removeCallbacks(currentCountingTimeRunable) | |||
| when { | |||
| isShowBackDialog -> { //显示返回弹窗 总计时不停,停止下一题和倒计时 | |||
| LogUtil.e("返回弹窗显示:移除下一题,移除单题倒计时") | |||
| mHandler.removeCallbacks(toNextRunable) | |||
| mHandler.removeCallbacks(currentCountingTimeRunnable) | |||
| } | |||
| else -> { //关闭返回弹窗 | |||
| //开启总计时 | |||
| startTotalCounting() | |||
| when{ | |||
| !isErrorPauseToNext ->{ // 暂停标志,不处理单题,非暂停 | |||
| when { | |||
| isGetNextIng -> { //是否获取下一题中 | |||
| //本来该获取下一题,现在获取下一题 | |||
| continueToNext() | |||
| } | |||
| else -> { //不是,进行当前题的计时 | |||
| mHandler.post(currentCountingTimeRunable) //现在恢复单题的计时 | |||
| } | |||
| } | |||
| } | |||
| } | |||
| else -> when { //关闭返回弹窗,错题暂停时,不处理,恢复下一题或单题倒计时 | |||
| isErrorPauseToNext -> {} | |||
| isGetNextIng -> continueToNext() | |||
| else -> mHandler.postDelayed(currentCountingTimeRunnable, currentCountingIntervalTime) | |||
| } | |||
| } | |||
| } | |||
| /** onResume | |||
| * isAllOver = true 结束了,不处理了 | |||
| * 开启总计时 | |||
| * 返回弹窗显示,则不处理 | |||
| * 返回弹窗未显示,则该获取下一题时获取下一题,该获取倒计时数据获取倒计时数据 | |||
| */ | |||
| override fun onResume(owner : LifecycleOwner) { | |||
| super.onResume(owner) | |||
| if (isAllOver) return | |||
| startTotalCounting() | |||
| isShowBackDialog = false | |||
| when{ | |||
| !isShowBackDialog -> { //没有显示返回弹窗,判断是否完成测试,没有完成,则进行计时,需要注意是否第一次进入,避免对第一题进行重复计时 | |||
| } | |||
| //弹窗未显示,可直接走弹窗关闭的流程 | |||
| !isShowBackDialog -> showOrDismissBackDialogForTime(isShowBackDialog) | |||
| } | |||
| } | |||
| /** onPause时,停止总计时 标记进入后台 如果弹窗显示,就只需要关闭总计时就可以了,否则关闭所有计时 */ | |||
| override fun onPause(owner : LifecycleOwner) { | |||
| super.onPause(owner) | |||
| //关闭所有计时,如果弹窗显示,就只需要关闭总计时就可以了,否则关闭所有计时 | |||
| isInBackground = true | |||
| //测试完成,直接返回,已经停止计时了 | |||
| if (isAllOver) return | |||
| //停止总计时、下一题、倒计时 | |||
| stopTotalCountTing() | |||
| when{ | |||
| //弹窗未显示且非错误暂停,移除计时,因为在弹窗显示和错误暂停的时候,已经移除了下一题和倒计时 | |||
| !isShowBackDialog || !isErrorPauseToNext -> { | |||
| mHandler.removeCallbacks(toNextRunable) | |||
| mHandler.removeCallbacks(currentCountingTimeRunnable) | |||
| } | |||
| } | |||
| } | |||
| // /** | |||
| // * 暂停或恢复计时 | |||
| // * @param isPause Boolean | |||
| // */ | |||
| // fun runAllTime(isPause:Boolean){ | |||
| // if (isAllOver) return | |||
| // this.isPause = isPause | |||
| // when{ | |||
| // isPause || isShowBackDialog -> { //暂停计时 | |||
| // //停止总计时 | |||
| // stopTotalCountTing() | |||
| // //移除下一条、单条计时 | |||
| // mHandler.removeCallbacksAndMessages(null) | |||
| // } | |||
| // else -> { //运行计时 | |||
| // //恢复总计时 | |||
| // startTotalCounting() | |||
| // //错误暂停不处理,只进行总计时,否则进行是否在获取下一题的判断 | |||
| // when { | |||
| // isPause -> when { //生命周期暂停后,需要恢复 | |||
| // !isErrorPauseToNext -> when { | |||
| // isGetNextIng -> continueToNext() //本来该获取下一题,现在获取下一题 | |||
| // else -> mHandler.post(currentCountingTimeRunable) //现在恢复单题的计时 | |||
| // } | |||
| // } | |||
| // } | |||
| // } | |||
| // } | |||
| // } | |||
| /** 停止获取下一题 */ | |||
| fun pauseToNext() { | |||
| LogUtil.e("停止获取下一题") | |||
| isErrorPauseToNext = true | |||
| mHandler.removeCallbacks(toNextRunable) | |||
| } | |||
| /** 继续获取下一题 */ | |||
| fun continueToNext() { | |||
| LogUtil.e("获取下一题") | |||
| isErrorPauseToNext = false | |||
| isGetNextIng = true | |||
| mHandler.post(toNextRunable) | |||
| } | |||
| /** 获取下一题的线程,同时判断是否完成 */ | |||
| @@ -229,10 +223,25 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| isGetNextIng = false | |||
| currentIndex++ | |||
| if (currentIndex >= testData.size) { | |||
| // TODO: 2022/4/11 标记学习已完成 停止计时 计算分数 设置总消费时间 | |||
| if (isAllOver) return | |||
| isAllOver = true | |||
| //停止计时 | |||
| stopTotalCountTing() | |||
| //计算分数 | |||
| calculateScore() | |||
| when(intentData.examType){ | |||
| AppConstants.TEST_TYPE_BEFORE -> { //学前总测为lesson添加错误数 与 分数 | |||
| intentData.lesson?.let { | |||
| it.errorNumber = it.errorNumber + newErrorMap.size | |||
| it.beforeTestScore = scoreValue.toDouble() | |||
| } | |||
| } | |||
| AppConstants.TEST_TYPE_AFTER -> { //学后测试为lesson添加分数 | |||
| intentData.lesson?.let { | |||
| it.afterTestScore = scoreValue.toDouble() | |||
| } | |||
| } | |||
| } | |||
| //封装数据 | |||
| packUpRecord() | |||
| //保存数据 后再使用 currentExamBean.value = null 都在saveData中 | |||
| @@ -256,6 +265,9 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| } | |||
| } | |||
| currentExamBean.value = nextExam | |||
| //开始倒计时 | |||
| LogUtil.e("开始单题倒计时") | |||
| mHandler.postDelayed(currentCountingTimeRunnable, currentCountingIntervalTime) | |||
| } | |||
| } | |||
| } | |||
| @@ -297,7 +309,7 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| /** 当前单词的计时 */ | |||
| private val currentCountingTimeRunable = object : Runnable { | |||
| private val currentCountingTimeRunnable = object : Runnable { | |||
| override fun run() { | |||
| when (intentData.courseType) { | |||
| AppConstants.COURSE_TYPE_ENGLISH_VOICE, AppConstants.COURSE_TYPE_ENGLISH_SPOKEN -> { // 累加时间 | |||
| @@ -310,32 +322,21 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| if (surplusTime > 0) { | |||
| mHandler.postDelayed(this, currentCountingIntervalTime) | |||
| } else { | |||
| chooseResult(-1) //-1为未答 | |||
| currentCountingTimeOver.value = true | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** 当前单词开始计时 */ | |||
| fun startCurrentCountingTime() { | |||
| isCurrentCounting = true | |||
| mHandler.postDelayed(currentCountingTimeRunable, currentCountingIntervalTime) | |||
| } | |||
| /** 移除单词计时 */ | |||
| fun stopCurrentCountingTime() { | |||
| isCurrentCounting = false | |||
| mHandler.removeCallbacks(currentCountingTimeRunable) | |||
| } | |||
| /** | |||
| * 四选一结果 | |||
| * @param position Int 位置 -1..3 ,-1 为未答 | |||
| */ | |||
| fun chooseResult(position : Int) { | |||
| //移除计时 | |||
| mHandler.removeCallbacks(currentCountingTimeRunable) | |||
| mHandler.removeCallbacks(currentCountingTimeRunnable) | |||
| LogUtil.e("结果移除单题倒计时") | |||
| //创建单题的测试记录 | |||
| currentExamRecord = ExamRecord.newBuilder().apply { | |||
| questionId = currentExamBean.value!!.word_id | |||
| @@ -389,7 +390,8 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| * */ | |||
| fun spellOver(selectedValue : String, errorSize : Int) { | |||
| //移除计时 | |||
| mHandler.removeCallbacks(currentCountingTimeRunable) | |||
| mHandler.removeCallbacks(currentCountingTimeRunnable) | |||
| LogUtil.e("结果移除单题倒计时") | |||
| //创建测试记录 | |||
| currentExamRecord = ExamRecord.newBuilder().apply { | |||
| questionId = currentExamBean.value!!.word_id | |||
| @@ -401,23 +403,27 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| val result : Long | |||
| when { | |||
| selectedValue.isEmpty() -> { //未答 | |||
| errorLiveData.value = errorLiveData.value!!.plus(1) //错误 | |||
| errorLiveData.value = errorLiveData.value!!.plus(1) | |||
| currentExamRecord!!.answerStatus = AppConstants.TEST_ERROR | |||
| toNextTime = AppConstants.TEST_TO_NEXT_ERROR_TIME | |||
| unAnswerNumber += 1 | |||
| currentExamRecord!!.answerStatus = AppConstants.TEST_UN_ANSWER | |||
| result = AppConstants.TEST_UN_ANSWER | |||
| //创建错误记录 | |||
| createErrorRecord() | |||
| } | |||
| errorSize > 0 -> { //错误 | |||
| errorLiveData.value = errorLiveData.value!!.plus(1) //错误 | |||
| errorLiveData.value = errorLiveData.value!!.plus(1) | |||
| currentExamRecord!!.answerStatus = AppConstants.TEST_ERROR | |||
| toNextTime = AppConstants.TEST_TO_NEXT_ERROR_TIME | |||
| result = AppConstants.TEST_TO_NEXT_ERROR_TIME | |||
| //创建错误记录 | |||
| createErrorRecord() | |||
| } | |||
| else -> { //正确 | |||
| correctLiveData.value = correctLiveData.value!!.plus(1) | |||
| currentExamRecord!!.answerStatus = AppConstants.TEST_CORRECT | |||
| toNextTime = AppConstants.TEST_CORRECT | |||
| toNextTime = AppConstants.TEST_TO_NEXT_CORRECT_TIME | |||
| result = AppConstants.TEST_CORRECT | |||
| } | |||
| } | |||
| @@ -547,7 +553,7 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| } | |||
| /** 封装试卷 */ | |||
| private fun packUpRecord() : Record.Builder { | |||
| private fun packUpRecord() { | |||
| val examType = intentData.examType | |||
| //生成试卷 | |||
| learnExam = LearnExam.newBuilder().apply { | |||
| @@ -570,7 +576,7 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| tag = "android" | |||
| created = DateUtil.format(System.currentTimeMillis(), DateUtil.FORMAT_1) //数据创建时间 | |||
| //courseType | |||
| val ext = "{\"courseType\": ${intentData.courseType} }" | |||
| ext = "{\"courseType\": ${intentData.courseType} }" | |||
| //词汇量测试: 覆盖率、阶段 | |||
| if (examType == AppConstants.TEST_TYPE_NORMAL) { | |||
| // TODO: 2022/4/14 设置词汇量测试的范围题目和覆盖率 | |||
| @@ -588,7 +594,6 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| // } | |||
| //最终上传数据 | |||
| val record = Record.newBuilder() //上传对象 | |||
| record.addExam(learnExam) //测试试卷 | |||
| //时间 | |||
| if (examType == AppConstants.TEST_TYPE_BEFORE_TOTAL || examType == AppConstants.TEST_TYPE_BEFORE || examType == AppConstants.TEST_TYPE_AFTER || examType == AppConstants.TEST_TYPE_AFTER_TOTAL) { | |||
| @@ -597,7 +602,6 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| if (mLearnEntities.size != 0) { | |||
| record.addAllEntity(mLearnEntities) | |||
| } | |||
| return record | |||
| } | |||
| /** | |||
| @@ -627,21 +631,57 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| it.onNext(true) | |||
| } | |||
| // TODO: 2022/4/14 传递保存record信息 | |||
| // record 已经实例化并已经将数据保存 | |||
| LogUtil.e(JsonFormat.printToString(record.build())) | |||
| }.compose(diskIo2Main()).subscribe({ | |||
| showHideLoading(false) | |||
| LiveDataBus.with<LearnEventData>(AppConstants.EVENT_COURSE).value = LearnEventData( | |||
| intentData.subjectId, | |||
| intentData.courseId, | |||
| AppConstants.DATA_COURSE_TEST_BEFORE).apply { | |||
| this.scoreValue = this@LearnExamViewModel.scoreValue | |||
| this.newErrorMap = this@LearnExamViewModel.newErrorMap | |||
| } | |||
| sendEventBus() //返回发送数据 | |||
| //数据保存完成后,通过数据为空进行通知,测试完成 | |||
| currentExamBean.value = null | |||
| }, { | |||
| showHideLoading(false) | |||
| it.printStackTrace() | |||
| }) | |||
| } | |||
| /** 发送事件 发送的key为EVENT_COURSE 但动作不同 */ | |||
| private fun sendEventBus(){ | |||
| when(intentData.examType){ | |||
| AppConstants.TEST_TYPE_BEFORE_TOTAL -> { //学前总测发送数据、学前总测发送数据 | |||
| LiveDataBus.withNonSticky<LearnEventData>(AppConstants.EVENT_COURSE).value = LearnEventData( | |||
| intentData.subjectId, | |||
| intentData.courseId, | |||
| AppConstants.DATA_COURSE_BEFORE_TEST_OVER).apply { | |||
| this.scoreValue = this@LearnExamViewModel.scoreValue | |||
| this.newErrorMap = this@LearnExamViewModel.newErrorMap | |||
| } | |||
| } | |||
| AppConstants.TEST_TYPE_BEFORE-> { // 课时学前测试结束发送数据 | |||
| LiveDataBus.withNonSticky<LearnEventData>(AppConstants.EVENT_COURSE).value = LearnEventData( | |||
| intentData.subjectId, | |||
| intentData.courseId, | |||
| AppConstants.DATA_LESSON_BEFORE_TEST_OVER).apply { | |||
| this.newErrorMap = this@LearnExamViewModel.newErrorMap | |||
| intentData.lesson?.let { | |||
| leesonPositionIndex = it.lessonPositionInList | |||
| } | |||
| } | |||
| } | |||
| AppConstants.TEST_TYPE_AFTER -> { //课时学后测试结束发送数据 | |||
| LiveDataBus.withNonSticky<LearnEventData>(AppConstants.EVENT_COURSE).value = LearnEventData( | |||
| intentData.subjectId, | |||
| intentData.courseId, | |||
| AppConstants.DATA_LESSON_AFTER_TEST_OVER).apply { | |||
| this.newErrorMap = this@LearnExamViewModel.newErrorMap | |||
| intentData.lesson?.let { | |||
| leesonPositionIndex = it.lessonPositionInList | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,12 +0,0 @@ | |||
| package com.xkl.cdl.module.learn | |||
| import androidx.appcompat.app.AppCompatActivity | |||
| import android.os.Bundle | |||
| import com.xkl.cdl.R | |||
| class LearnSpellActivity : AppCompatActivity() { | |||
| override fun onCreate(savedInstanceState: Bundle?) { | |||
| super.onCreate(savedInstanceState) | |||
| setContentView(R.layout.activity_learn_spell) | |||
| } | |||
| } | |||
| @@ -4,6 +4,9 @@ import androidx.appcompat.app.AppCompatActivity | |||
| import android.os.Bundle | |||
| import com.xkl.cdl.R | |||
| /** | |||
| * 正常的学习、复习、自动播放 | |||
| */ | |||
| class LearnWordActivity : AppCompatActivity() { | |||
| override fun onCreate(savedInstanceState: Bundle?) { | |||
| super.onCreate(savedInstanceState) | |||
| @@ -4,10 +4,17 @@ import android.view.View | |||
| import androidx.lifecycle.ViewModelProvider | |||
| import androidx.recyclerview.widget.LinearLayoutManager | |||
| import com.suliang.common.base.fragment.BaseFragmentVM | |||
| import com.suliang.common.eventbus.LiveDataBus | |||
| import com.xkl.cdl.adapter.AdapterLesson | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.bean.LearnDialogBean | |||
| import com.xkl.cdl.data.bean.course.ExamBean | |||
| import com.xkl.cdl.data.bean.course.Lesson | |||
| import com.xkl.cdl.data.bean.intentdata.ExamData | |||
| import com.xkl.cdl.data.event.LearnEventData | |||
| import com.xkl.cdl.data.manager.CourseManager | |||
| import com.xkl.cdl.databinding.FragmentCourseLessonBinding | |||
| import com.xkl.cdl.dialog.LearnDialog | |||
| /** | |||
| * 课程章节目录 | |||
| @@ -22,6 +29,7 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| return ViewModelProvider(requireParentFragment())[CourseMainFragmentViewModel::class.java] | |||
| } | |||
| private lateinit var adapterLesson: AdapterLesson | |||
| override fun initFragment() { | |||
| //口语显示顶部的自动播放与跟读模式 | |||
| if (vm.course.courseType == AppConstants.COURSE_TYPE_ENGLISH_SPOKEN){ | |||
| @@ -29,31 +37,87 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| } | |||
| binding.recyclerView.apply { | |||
| layoutManager = LinearLayoutManager(requireContext(),LinearLayoutManager.VERTICAL,false) | |||
| adapter = AdapterLesson(vm).apply { | |||
| adapterLesson = AdapterLesson(vm).apply { | |||
| onItemClick = onLessonClick | |||
| } | |||
| adapter = adapterLesson | |||
| } | |||
| //设置数据 | |||
| (binding.recyclerView.adapter as AdapterLesson).setData(vm.allLesson.toMutableList()) | |||
| } | |||
| override fun loadData() { | |||
| //监听数据传递事件 | |||
| listenerLiveBus() | |||
| } | |||
| private fun listenerLiveBus(){ | |||
| //监听数据 | |||
| LiveDataBus.withNonSticky<LearnEventData>(AppConstants.EVENT_COURSE).observe(this) { learnEventData -> | |||
| if (learnEventData.subjectId != vm.course.subjectId || learnEventData.courseId != vm.course.courseId) return@observe | |||
| when (learnEventData.actionFlag) { | |||
| //课时学前测试结束,传递数据回来更新数据 | |||
| AppConstants.DATA_LESSON_BEFORE_TEST_OVER -> { | |||
| // 更新Lesson 更新detail课时学前成绩,错误条目数,测试前错误列表 | |||
| adapterLesson.notifyItemChanged(learnEventData.leesonPositionIndex) | |||
| //获取lesson | |||
| vm.allLesson[learnEventData.leesonPositionIndex].let { | |||
| val key = "${it.chapterId}_${it.lessonId}" | |||
| vm.courseDetail.before.put(key,it.beforeTestScore) | |||
| vm.courseDetail.wrong.put(key,it.errorNumber) | |||
| learnEventData.newErrorMap?.let { it1 -> | |||
| vm.courseDetail.exam_w_r_list.putAll(it1) | |||
| } | |||
| } | |||
| } | |||
| //课时学前测试结束,开始学习 -> 进入学习界面 | |||
| AppConstants.ACTION_LESSON_BEFORE_TEST_OVER_START_LEARN -> { | |||
| } | |||
| //学后测试结束传递数据回来更新数据 | |||
| AppConstants.DATA_LESSON_AFTER_TEST_OVER -> { | |||
| //更新lesson 更新detail成绩 | |||
| adapterLesson.notifyItemChanged(learnEventData.leesonPositionIndex) | |||
| vm.allLesson[learnEventData.leesonPositionIndex].let { | |||
| val key = "${it.chapterId}_${it.lessonId}" | |||
| vm.courseDetail.after.put(key,it.beforeTestScore) | |||
| } | |||
| } | |||
| //学后测试结束,弹窗动作 | |||
| AppConstants.ACTION_LESSON_AFTER_TEST_RELEARN -> { | |||
| //课时学后测试,点击重学 | |||
| // TODO: 2022/4/22 清除当前课时数据,并更新当前课时,后进入学习界面 | |||
| learnEventData.leesonPositionIndex | |||
| } | |||
| AppConstants.ACTION_LESSON_AFTER_TEST_AGAIN -> { | |||
| //学后测试,再测一次 | |||
| loadLessonAfterTest(vm.allLesson[learnEventData.leesonPositionIndex]) | |||
| } | |||
| AppConstants.ACTION_LESSON_AFTER_TEST_NEXT -> { | |||
| //下一课时 or 下一步(提示课程学习完成) | |||
| // TODO: 2022/4/22 判断课程是否学习完成,学习完成,显示课程学习完成弹窗,否则进入下一个没有学习完成的课时,进行学习 | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** 课时点击实现 */ | |||
| private val onLessonClick : (v:View,position: Int, leeson: Lesson) -> Unit = { view, position, entity -> | |||
| // TODO: 2022/3/31 根据情况具体跳转实现 学前、学习、学后 | |||
| private val onLessonClick : (v:View,position: Int, lesson: Lesson) -> Unit = { view, position, entity -> | |||
| when(entity.lessonType){ | |||
| AppConstants.LESSON_TYPE_WORD -> { | |||
| if (entity.beforeTestScore == AppConstants.NOT_DOING){ | |||
| if (entity.beforeTestScore == AppConstants.NOT_DOING){ //课时学前测试,没有做 | |||
| //弹窗显示学前测试提示 | |||
| showLessonBeforeTestStartDialog(entity) | |||
| }else if (!entity.learnIsOver){ | |||
| }else if (entity.afterTestScore != AppConstants.NOT_DOING){ | |||
| }else{ | |||
| loadLessonAfterTest(entity) | |||
| }else{ //当前课时学习完成的弹窗 | |||
| showLessonAllOverDialog(entity) | |||
| } | |||
| } | |||
| AppConstants.LESSON_TYPE_SENTENCE -> { | |||
| @@ -82,5 +146,79 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| } | |||
| } | |||
| /** 显示课时前测试开始的弹窗 | |||
| * 先获取数据,然后显示弹窗 | |||
| * */ | |||
| private fun showLessonBeforeTestStartDialog(lesson:Lesson){ | |||
| vm.loadTest(AppConstants.TEST_TYPE_BEFORE,lesson).observe(this){ | |||
| //对话框信息实体 | |||
| val learnDialogBean = LearnDialogBean(AppConstants.DIALOG_TYPE_EXAM_START).apply { | |||
| examType = AppConstants.TEST_TYPE_BEFORE | |||
| chapter_lesson_name = "${lesson.chapterName} ${lesson.lessonName}" | |||
| showTimeCount = CourseManager.expectedTestTime(vm.course.courseType, AppConstants.TEST_TYPE_BEFORE, it) | |||
| } | |||
| LearnDialog.newInstance(learnDialogBean).apply { | |||
| onDialogListener = { action, dialog -> | |||
| dialog.dismissAllowingStateLoss() | |||
| when (action) { | |||
| AppConstants.DIALOG_START_LEARN -> { //跳过测试 继续学习 | |||
| // TODO: 2022/4/21 进入学习界面 | |||
| } | |||
| // 开始测试 | |||
| AppConstants.DIALOG_START_TEST -> startLessonTest(lesson,AppConstants.TEST_TYPE_BEFORE,it) | |||
| } | |||
| } | |||
| }.show(childFragmentManager, "lesson_before_test_start") | |||
| } | |||
| } | |||
| /** 开始课时学后测试 | |||
| * 请求数据后,直接开始跳转 | |||
| */ | |||
| private fun loadLessonAfterTest(lesson : Lesson){ | |||
| vm.loadTest(AppConstants.TEST_TYPE_AFTER,lesson).observe(this){ | |||
| startLessonTest(lesson,AppConstants.TEST_TYPE_AFTER,it) | |||
| } | |||
| } | |||
| /** 调用数据,开始测试 */ | |||
| private fun startLessonTest(lesson: Lesson, examType:Int,testData : List<ExamBean>){ | |||
| //生成数据 | |||
| val examData = ExamData(vm.course.subjectId, examType, | |||
| lesson.lessonName, | |||
| "${vm.course.courseTitle} ${lesson.chapterName} ${lesson.lessonName}").apply { | |||
| coursePackId = vm.course.coursePackId | |||
| coursePackType = vm.course.coursePackType | |||
| courseId = vm.course.courseId | |||
| courseType = vm.course.courseType | |||
| this.lesson = lesson | |||
| this.testData = testData | |||
| mExamWRMap = vm.courseDetail.exam_w_r_list | |||
| } | |||
| (this@CourseLessonFragment.parentFragment as CourseMainFragment).startExam(examData) | |||
| } | |||
| /** lesson的学习 lessonType 为 word类型 */ | |||
| private fun startLessonLearnForWord(){ | |||
| } | |||
| /** | |||
| * 当前课时学前、学习、学后都完成了的弹窗 | |||
| */ | |||
| private fun showLessonAllOverDialog(lesson : Lesson){ | |||
| } | |||
| /** | |||
| * 显示课程学习完成的弹窗 | |||
| */ | |||
| private fun showCourseOverDialog(){ | |||
| } | |||
| } | |||
| @@ -38,26 +38,26 @@ class CourseMainFragment : BaseFragmentVM<FragmentCourseMainBinding, CourseMainF | |||
| return ViewModelProvider(this, | |||
| ViewModelFactory(requireArguments().getInt(AppConfig.INTENT_1)))[CourseMainFragmentViewModel::class.java].apply { | |||
| coursePackMainActivityVM = ViewModelProvider(requireActivity())[CoursePackMainActivityViewModel::class.java] | |||
| LogUtil.e("CourseMainFragment coursePackMainActivityVM hashCode -> ${coursePackMainActivityVM.hashCode()}") | |||
| // LogUtil.e("CourseMainFragment coursePackMainActivityVM hashCode -> ${coursePackMainActivityVM.hashCode()}") | |||
| } | |||
| } | |||
| override fun initFragment() { | |||
| //设置课程 和 需要操作的类型 | |||
| vm.course = vm.coursePackMainActivityVM.coursePack.childrenCourses[vm.courseIndex].apply { | |||
| vm.dbControlBase = DbControlBase(subjectId, coursePackId, courseId, courseType) | |||
| vm.dbControlBase = DbControlBase(subjectId, coursePackId,coursePackType, courseId, courseType) | |||
| } | |||
| //监听动作 总测完成,切换到目录页 | |||
| LiveDataBus.with<LearnEventData>(AppConstants.EVENT_COURSE).observe(this) { | |||
| LiveDataBus.withNonSticky<LearnEventData>(AppConstants.EVENT_COURSE).observe(this) { | |||
| if (it.subjectId != vm.course.subjectId || it.courseId != vm.course.courseId) return@observe | |||
| when (it.actionFlag) { | |||
| // 学前总测、学后总测 之继续学习 -> 切换到目录页 | |||
| AppConstants.ACTION_COURSE_TEST_START_LEARN -> changeFragment(1) | |||
| //学前总测结束,传递数据回来更新数据 | |||
| AppConstants.DATA_COURSE_TEST_BEFORE -> { | |||
| AppConstants.DATA_COURSE_BEFORE_TEST_OVER -> { | |||
| vm.courseDetail.st_before = it.scoreValue.toDouble() //学前总分 | |||
| it.newErrorMap?.run { | |||
| when { | |||
| @@ -70,13 +70,8 @@ class CourseMainFragment : BaseFragmentVM<FragmentCourseMainBinding, CourseMainF | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| override fun loadData() { | |||
| @@ -85,8 +80,6 @@ class CourseMainFragment : BaseFragmentVM<FragmentCourseMainBinding, CourseMainF | |||
| vm.loadMain().observe(this) { | |||
| changeChildrenFragment() | |||
| } | |||
| } | |||
| @@ -132,6 +125,12 @@ class CourseMainFragment : BaseFragmentVM<FragmentCourseMainBinding, CourseMainF | |||
| LearnExamActivity.newInstance(requireContext()) | |||
| } | |||
| override fun onDestroy() { | |||
| super.onDestroy() | |||
| //退出本课程后移除该课程的事件监听 | |||
| // LiveDataBus.remove(AppConstants.EVENT_COURSE) | |||
| } | |||
| inner class ViewModelFactory(private val courseIndex : Int) : ViewModelProvider.Factory { | |||
| override fun <T : ViewModel?> create(modelClass : Class<T>) : T { | |||
| @@ -1,21 +1,20 @@ | |||
| package com.xkl.cdl.module.m_center_learn.coursechildren | |||
| import androidx.lifecycle.MutableLiveData | |||
| import com.suliang.common.base.activity.ToastEvent | |||
| import com.suliang.common.base.viewmodel.BaseViewModel | |||
| import com.suliang.common.extension.diskIo2DiskIo | |||
| import com.suliang.common.extension.diskIo2Main | |||
| import com.xkl.cdl.data.bean.LearnWord | |||
| import com.xkl.cdl.data.bean.course.Course | |||
| import com.xkl.cdl.data.bean.course.CourseDetail | |||
| import com.xkl.cdl.data.bean.course.ExamBean | |||
| import com.xkl.cdl.data.bean.course.Lesson | |||
| import com.xkl.cdl.data.bean.intentdata.LearnData | |||
| import com.xkl.cdl.data.manager.db.DBCourseManager | |||
| import com.xkl.cdl.data.manager.db.DbControlBase | |||
| import com.xkl.cdl.data.repository.DataRepository | |||
| import com.xkl.cdl.module.m_center_learn.CoursePackMainActivityViewModel | |||
| import io.reactivex.rxjava3.core.Observable | |||
| import io.reactivex.rxjava3.core.Observer | |||
| import io.reactivex.rxjava3.disposables.Disposable | |||
| import io.reactivex.rxjava3.functions.BiFunction | |||
| import java.util.* | |||
| import kotlin.collections.HashMap | |||
| @@ -53,37 +52,22 @@ class CourseMainFragmentViewModel(val courseIndex : Int) : BaseViewModel() { | |||
| //获取课程的复习数据 | |||
| //获取课程的章节数据 | |||
| fun loadMain() : MutableLiveData<Boolean> { | |||
| showHideLoading(true) | |||
| // showHideLoading(true) | |||
| val mutableLiveData = MutableLiveData<Boolean>() | |||
| Observable.concat(DataRepository.getCourseStatistics().flatMap { | |||
| Observable.zip(DataRepository.getCourseStatistics().flatMap { | |||
| courseDetail = it | |||
| return@flatMap DataRepository.getCourseAllLesson(dbControlBase, it) | |||
| }, DataRepository.getReviewData(), DataRepository.getCourseCollect()).compose(diskIo2DiskIo()) | |||
| .subscribe(object : Observer<Any> { | |||
| override fun onSubscribe(d : Disposable?) { | |||
| } | |||
| override fun onNext(t : Any?) { | |||
| t?.let { it -> | |||
| if (it is List<*>) { | |||
| allLesson = it as List<Lesson> | |||
| } | |||
| } | |||
| } | |||
| override fun onError(e : Throwable?) { | |||
| e?.printStackTrace() | |||
| showHideLoading(false) | |||
| showToast(ToastEvent(content = "数据加载异常")) | |||
| } | |||
| override fun onComplete() { | |||
| showHideLoading(false) | |||
| mutableLiveData.postValue(true) | |||
| } | |||
| }) | |||
| }.flatMap { | |||
| allLesson = it | |||
| return@flatMap DataRepository.getCourseCollect() | |||
| }, | |||
| DataRepository.getReviewData(), | |||
| BiFunction<HashMap<String, Long>, Array<String>, Boolean> { t1 : HashMap<String, Long>?, t2 : Array<String>? -> | |||
| true | |||
| }).compose(diskIo2Main()).subscribe { | |||
| // showHideLoading(false) | |||
| mutableLiveData.value = it | |||
| } | |||
| return mutableLiveData | |||
| } | |||
| @@ -94,16 +78,13 @@ class CourseMainFragmentViewModel(val courseIndex : Int) : BaseViewModel() { | |||
| */ | |||
| fun loadTest(testType : Int, lesson : Lesson? = null) : MutableLiveData<List<ExamBean>> { | |||
| val result = MutableLiveData<List<ExamBean>>() | |||
| // showHideLoading(true) | |||
| Observable.create<List<ExamBean>> { | |||
| it.onNext(DBCourseManager.queryLearnTest(dbControlBase, testType, lesson)) | |||
| it.onComplete() | |||
| }.compose(diskIo2Main()).subscribe({ | |||
| // showHideLoading(false) | |||
| result.value = it | |||
| }, { | |||
| it.printStackTrace() | |||
| // showHideLoading(false) | |||
| }) | |||
| return result | |||
| @@ -115,21 +96,41 @@ class CourseMainFragmentViewModel(val courseIndex : Int) : BaseViewModel() { | |||
| fun modifyLessonErrorNumber() { | |||
| //重设detail中的错误数, exam_w_r_list为所有的测试前错误,因为时学前总测,所以,可以根据这个字段来计算每个课时的错误数 | |||
| //记录错误数: key :chapterId_lessonId value :错误数 | |||
| val errorNumber = hashMapOf<String,Int>() | |||
| val errorNumber = hashMapOf<String, Int>() | |||
| courseDetail.exam_w_r_list.keys.forEach { | |||
| val split = it.split("_") | |||
| if (split.size == 3){ | |||
| if (split.size == 3) { | |||
| val errorKey = "${split[0]}_${split[1]}" | |||
| errorNumber[errorKey] = errorNumber.getOrElse(errorKey,{0}) + 1 | |||
| errorNumber[errorKey] = errorNumber.getOrElse(errorKey, { 0 }) + 1 | |||
| } | |||
| } | |||
| courseDetail.wrong.putAll(errorNumber) | |||
| //更新lesson中的错误数 | |||
| allLesson.forEach { | |||
| val key = "${it.chapterId}_${it.lessonId}" | |||
| it.errorNumber = errorNumber.getOrElse(key,{0}) | |||
| it.errorNumber = errorNumber.getOrElse(key, { 0 }) | |||
| } | |||
| } | |||
| /** | |||
| * 查询获取学习数据 单词类、口语词汇、句型, 音标课程、识字、拼音、作文知识点学习 | |||
| * @param lesson Lesson 课时 | |||
| * @return MutableLiveData<LearnData> | |||
| */ | |||
| fun loadLessonLearnForWord(lesson : Lesson): MutableLiveData<LearnData>{ | |||
| val result = MutableLiveData<LearnData>() | |||
| Observable.create<List<LearnWord>> { | |||
| it.onNext(DBCourseManager.queryLearnDataForWord(dbControlBase, lesson)) | |||
| it.onComplete() | |||
| }.compose(diskIo2Main()) | |||
| .subscribe { | |||
| result.value = LearnData(lesson).apply { | |||
| learnWordList = it | |||
| } | |||
| } | |||
| return result | |||
| } | |||
| } | |||
| @@ -59,6 +59,8 @@ class CourseTotalTestFragment : BaseFragmentVM<FragmentCourseTotalTestBinding, C | |||
| binding.tvCountTip.text = CourseManager.expectedTestTime(vm.course.courseType,totalTestType,testData!!) | |||
| when (totalTestType) { | |||
| AppConstants.TEST_TYPE_BEFORE_TOTAL -> { | |||
| // TODO: 2022/4/21 需要把开始学习给取消了 | |||
| binding.button1.visibility = View.VISIBLE | |||
| //学前总测 | |||
| binding.run { | |||
| tvTitle.setText(R.string.test_total_before_title) | |||
| @@ -3,20 +3,11 @@ package com.xkl.cdl.module.splash | |||
| import android.annotation.SuppressLint | |||
| import android.os.Bundle | |||
| import com.suliang.common.base.activity.BaseActivity | |||
| import com.suliang.common.util.LogUtil | |||
| import com.suliang.common.util.SpUtils | |||
| import com.suliang.common.util.file.FileUtil | |||
| import com.suliang.common.util.thread.AppExecutors | |||
| import com.xkl.cdl.data.manager.CourseManager | |||
| import com.xkl.cdl.data.manager.FilePathManager | |||
| import com.xkl.cdl.data.manager.db.DbCoursePackManager | |||
| import com.xkl.cdl.databinding.ActivitySplashBinding | |||
| import com.xkl.cdl.dialog.LearnDialog | |||
| import com.xkl.cdl.module.main.MainActivity | |||
| import mobile_cache.Mobile_cache | |||
| import java.io.File | |||
| import java.util.concurrent.TimeUnit | |||
| @SuppressLint("CustomSplashScreen") | |||
| @@ -0,0 +1,103 @@ | |||
| package com.xkl.cdl.util | |||
| import com.xkl.cdl.data.bean.BaseWord | |||
| /** | |||
| * author suliang | |||
| * create 2022/4/22 17:12 | |||
| * Describe: 规则 | |||
| * @property originList 源数据 | |||
| * @property cycleTime 循环次数 | |||
| */ | |||
| class LearnRuleUtil<T : BaseWord>(private val originList : List<T>, private val cycleTime : Int) { | |||
| //源数据学习位置 | |||
| private var originLearnPosition : Int = 0 | |||
| //学习列表位置 | |||
| private var currentLearnPosition : Int = -1 | |||
| //学习列表数据 | |||
| private var currentList = mutableListOf<T?>(null) | |||
| //当前答题是否错误 | |||
| private var currentIsError = false | |||
| //学前总和课时前测试错误集合 | |||
| var examErrorMap = hashMapOf<String,Boolean>() | |||
| //新错误集合 | |||
| var newErrorMap = hashMapOf<String,Boolean>() | |||
| //第一次点击正确的集合, 如果后面再点击错误,需要移除并添加到错误集合中 | |||
| var newCorrectMap = hashMapOf<String,Boolean>() | |||
| fun getNext(): T? { | |||
| if (currentIsError || isHaveNext()){ | |||
| return nextWord() | |||
| } | |||
| return null | |||
| } | |||
| /** | |||
| * 是否还有下一单词 | |||
| * @return Boolean | |||
| */ | |||
| private fun isHaveNext():Boolean{ | |||
| //如果当前学习下标为-1,即第一次进入学习,直接返true | |||
| if (currentLearnPosition == -1) return true | |||
| //元数据是否学习完成,未学习完成,则还有学习数据, 元数据学习完成,还需要判断当前学习列表是否学习完成 | |||
| if (!isLearnOverForOrigin()) return true | |||
| //判断当前列表是否学习完成 | |||
| val currentLearnWord = currentList[currentLearnPosition]!! | |||
| var nextWord: T? = null | |||
| //循环找寻与当前word不一样的数据 | |||
| if (currentLearnPosition + 1 < currentList.size-1) { | |||
| for (i in currentLearnPosition + 1 .. currentList.size - 1) { | |||
| currentList[i]?.let { | |||
| if (currentLearnWord.wordId != it.wordId ){ | |||
| nextWord = it | |||
| } | |||
| } | |||
| if (nextWord != null) break | |||
| } | |||
| } | |||
| //如果获取不到下一个不为空的数据,则还有下一条,否则结束了 | |||
| return nextWord != null | |||
| } | |||
| /** | |||
| * 源数据是否学习完成 | |||
| */ | |||
| fun isLearnOverForOrigin():Boolean{ | |||
| return originLearnPosition >= originList.size -1 | |||
| } | |||
| /** | |||
| * 获取下一条数据 | |||
| */ | |||
| private fun nextWord(): T?{ | |||
| //声明下一条数据 | |||
| var next : T? = null | |||
| //当前列表 学习位置+1 | |||
| currentLearnPosition ++ | |||
| when{ | |||
| //从当前集合获取 | |||
| currentList.size > currentLearnPosition -> { | |||
| } | |||
| //从源数据据列表获取 | |||
| else -> { | |||
| originLearnPosition++ | |||
| if (!isLearnOverForOrigin()) { | |||
| next = originList[originLearnPosition] | |||
| //元数据如果是课时学前或总测的错误数据,那么就需要按照小循环插入 | |||
| } | |||
| } | |||
| } | |||
| return next | |||
| } | |||
| /** 是否在错误列表中 */ | |||
| } | |||
| @@ -1,9 +0,0 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| xmlns:app="http://schemas.android.com/apk/res-auto" | |||
| xmlns:tools="http://schemas.android.com/tools" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="match_parent" | |||
| tools:context=".module.learn.LearnExamSpellActivity"> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| @@ -1,9 +0,0 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| xmlns:app="http://schemas.android.com/apk/res-auto" | |||
| xmlns:tools="http://schemas.android.com/tools" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="match_parent" | |||
| tools:context=".module.learn.LearnSpellActivity"> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| @@ -82,9 +82,7 @@ | |||
| app:layout_constraintStart_toEndOf="@+id/tv_left" | |||
| android:background="@color/gray_1" | |||
| app:layout_constraintTop_toTopOf="@+id/tv_right" | |||
| app:layout_constraintBottom_toBottomOf="@+id/tv_right" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| app:layout_constraintBottom_toBottomOf="@+id/tv_right"/> | |||
| <!--左边按钮--> | |||
| <TextView | |||
| @@ -99,8 +97,6 @@ | |||
| app:layout_constraintEnd_toStartOf="@+id/vSplit" | |||
| app:layout_constraintTop_toTopOf="@+id/vSplit" | |||
| app:layout_constraintBottom_toBottomOf="@+id/vSplit" | |||
| android:visibility="gone" | |||
| tools:visibility="visible" | |||
| /> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| @@ -31,8 +31,7 @@ | |||
| android:src="@drawable/ic_delete" | |||
| android:visibility="gone" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintTop_toTopOf="parent" | |||
| tools:visibility="visible" /> | |||
| app:layout_constraintTop_toTopOf="parent"/> | |||
| <TextView | |||
| @@ -46,8 +45,7 @@ | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toTopOf="parent" | |||
| tools:text="%d分" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| android:visibility="gone"/> | |||
| <TextView | |||
| android:id="@+id/tv_tip" | |||
| @@ -59,8 +57,7 @@ | |||
| app:layout_constraintEnd_toEndOf="@+id/tv_score" | |||
| app:layout_constraintStart_toStartOf="@+id/tv_score" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_score" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| android:visibility="gone"/> | |||
| <TextView | |||
| android:id="@+id/tv_title" | |||
| @@ -75,8 +72,7 @@ | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_tip" | |||
| tools:text="课时学前测试" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| android:visibility="gone"/> | |||
| <TextView | |||
| android:id="@+id/tv_lesson_name" | |||
| @@ -92,8 +88,7 @@ | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_title" | |||
| tools:text="[%s%s]" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| android:visibility="gone"/> | |||
| <TextView | |||
| android:id="@+id/tv_count_time" | |||
| @@ -106,8 +101,7 @@ | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_lesson_name" | |||
| tools:text="@string/test_count_time_format" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| android:visibility="gone"/> | |||
| <TextView | |||
| android:id="@+id/tv_tip_1" | |||
| @@ -120,8 +114,7 @@ | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_title" | |||
| tools:text="@string/test_before_test_over_tip" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| android:visibility="gone"/> | |||
| <include | |||
| android:id="@+id/inc_statistics_number" | |||
| @@ -131,8 +124,36 @@ | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_tip_1" | |||
| android:visibility="gone"/> | |||
| <TextView | |||
| android:id="@+id/tv_top" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginTop="@dimen/global_spacing" | |||
| android:gravity="center" | |||
| tools:text="再测一次" | |||
| android:textColor="@color/theme_color" | |||
| android:textSize="@dimen/smallSize" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@id/inc_statistics_number" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| /> | |||
| <TextView | |||
| android:id="@+id/tv_top_1" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginTop="10dp" | |||
| android:gravity="center" | |||
| tools:text="再测一次" | |||
| android:textColor="@color/theme_color" | |||
| android:textSize="@dimen/smallSize" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@id/tv_top" | |||
| android:visibility="gone" | |||
| /> | |||
| @@ -142,12 +163,12 @@ | |||
| android:layout_height="wrap_content" | |||
| app:barrierDirection="bottom" | |||
| app:barrierMargin="@dimen/smallerSize" | |||
| app:constraint_referenced_ids="tv_count_time,inc_statistics_number" /> | |||
| app:constraint_referenced_ids="tv_count_time,inc_statistics_number,tv_top,tv_top_1" /> | |||
| <View | |||
| android:id="@+id/bottom_top_line" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="@dimen/line_height" | |||
| android:layout_height="1dp" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| android:background="@color/gray_3" | |||
| @@ -169,15 +190,14 @@ | |||
| <View | |||
| android:id="@+id/vSplit" | |||
| android:layout_width="@dimen/line_height" | |||
| android:layout_width="1dp" | |||
| android:layout_height="0dp" | |||
| app:layout_constraintEnd_toStartOf="@+id/tv_right" | |||
| app:layout_constraintStart_toEndOf="@+id/tv_left" | |||
| android:background="@color/gray_3" | |||
| app:layout_constraintTop_toTopOf="@+id/tv_right" | |||
| app:layout_constraintBottom_toBottomOf="@+id/tv_right" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| android:visibility="gone"/> | |||
| <!--左边按钮--> | |||
| <TextView | |||
| @@ -193,15 +213,36 @@ | |||
| app:layout_constraintTop_toTopOf="@+id/vSplit" | |||
| app:layout_constraintBottom_toBottomOf="@+id/vSplit" | |||
| android:visibility="gone" | |||
| tools:visibility="visible" | |||
| /> | |||
| <!--学前总测试控制显示的布局id--> | |||
| <!-- <!–学前总测试控制显示的布局id–>--> | |||
| <!-- <androidx.constraintlayout.widget.Group--> | |||
| <!-- android:id="@+id/group_total_test"--> | |||
| <!-- android:layout_width="wrap_content"--> | |||
| <!-- android:layout_height="wrap_content"--> | |||
| <!-- app:constraint_referenced_ids="tv_score,tv_tip_1,tv_title,inc_statistics_number"--> | |||
| <!-- android:visibility="invisible"--> | |||
| <!-- />--> | |||
| <!-- <!–课时学前测试开始控制显示的布局id–>--> | |||
| <!-- <androidx.constraintlayout.widget.Group--> | |||
| <!-- android:id="@+id/group_before_test_start"--> | |||
| <!-- android:layout_width="wrap_content"--> | |||
| <!-- android:layout_height="wrap_content"--> | |||
| <!-- app:constraint_referenced_ids="tv_title,tv_lesson_name,tv_count_time,tv_left,vSplit"--> | |||
| <!-- />--> | |||
| <!--课时学前测试结束绑定的布局组 --> | |||
| <!-- | |||
| <androidx.constraintlayout.widget.Group | |||
| android:id="@+id/group_before_test_start" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| app:constraint_referenced_ids="tv_score,tv_tip,tv_tip_1,tv_title,inc_statistics_number" | |||
| /> | |||
| --> <!--课时学后测试结束绑定的布局组 --> | |||
| <androidx.constraintlayout.widget.Group | |||
| android:id="@+id/group_total_test" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| app:constraint_referenced_ids="tv_score,tv_tip_1,tv_title,inc_statistics_number" | |||
| app:constraint_referenced_ids="tv_score,tv_tip,tv_title,inc_statistics_number,tv_top,tv_left,vSplit,tv_top_1" | |||
| /> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| @@ -77,11 +77,11 @@ | |||
| android:layout_marginStart="10dp" | |||
| android:layout_marginEnd="10dp" | |||
| android:background="@color/red_3" | |||
| app:layout_constraintLeft_toLeftOf="parent" | |||
| app:layout_constraintRight_toRightOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_rule_tip" | |||
| android:layout_marginTop="12dp" | |||
| app:layout_constrainedWidth="true" /> | |||
| app:layout_constrainedWidth="true"/> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| @@ -59,11 +59,14 @@ | |||
| <string name="test_spell_tip">请从左至右依次点击选择正确的选项</string> | |||
| <string name="pause">暂停</string> | |||
| <string name="continue_">继续</string> | |||
| <string name="test_tip">请选择本单词正确的意义栏</string> | |||
| <string name="test_tip">请选择正确的意义栏</string> | |||
| <string name="dialog_test_not_over">测试还未结束,你确定要退出吗?</string> | |||
| <string name="dialog_test_not_over_tip">退出后系统将不会保存你的本次测试数据</string> | |||
| <string name="quit">退出</string> | |||
| <string name="cancel">取消</string> | |||
| <string name="lesson_relearn_title">重新学习将清空本课时的学习记录</string> | |||
| <string name="lesson_relearn_content">但不会清空测试与备忘本数据</string> | |||
| <string name="sure">确定</string> | |||
| </resources> | |||
| @@ -10,4 +10,7 @@ object AppConfig { | |||
| const val INTENT_2 = "intent_2" | |||
| const val INTENT_3 = "intent_3" | |||
| const val INTENT_4 = "intent_4" | |||
| //音量存放地址 | |||
| val VOICE : String = "voice" | |||
| } | |||
| @@ -20,7 +20,6 @@ abstract class BaseFragmentVM<VB : ViewBinding, VM : BaseViewModel> : BaseFragme | |||
| vm.pageEvent.observe(this) { startActivity(it) } | |||
| vm.toastEvent.observe(this){ showToast(it) } | |||
| vm.loadingEvent.observe(this) { | |||
| LogUtil.e("监听到 fragment vm 的 loadingEvent isShow $it") | |||
| showHideLoading(it) | |||
| } | |||
| } | |||
| @@ -11,18 +11,19 @@ import java.lang.reflect.Method | |||
| /** | |||
| * author suliang | |||
| * create 2022/3/15 10:51 | |||
| * Describe: 非粘性LiveData | |||
| * Describe: 非粘性LiveData ,需要先设置监听再发送数据,即后注册的监听收不到以前的监听 | |||
| */ | |||
| class NonStickyMutableLiveData<T> : MutableLiveData<T>() { | |||
| private var stickFlag : Boolean = false | |||
| // private var stickFlag : Boolean = false | |||
| override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { | |||
| super.observe(owner, observer) | |||
| if (!stickFlag){ | |||
| hook(observer) | |||
| stickFlag = true | |||
| } | |||
| hook(observer) | |||
| // if (!stickFlag){ | |||
| // hook(observer) | |||
| // stickFlag = true | |||
| // } | |||
| } | |||
| private fun hook(observer: Observer<in T>) = try { | |||
| @@ -18,6 +18,7 @@ import com.suliang.common.util.LogUtil | |||
| fun View.click(clickListener : ((view : View) -> Unit)?){ | |||
| if (clickListener == null) { | |||
| setOnClickListener(null) | |||
| isClickable = false | |||
| return | |||
| } | |||
| val minTime = 500L | |||
| @@ -36,7 +36,7 @@ object FileUtil { | |||
| file?.let { | |||
| if (it.exists()) file.mkdirs() | |||
| } | |||
| LogUtil.d("应用内置目录文件: ${file?.path}") | |||
| // LogUtil.d("应用内置目录文件: ${file?.path}") | |||
| return file ?: throw NullPointerException("Expression 'file' must not be null") | |||
| } | |||
| @@ -16,7 +16,7 @@ interface IMP { | |||
| fun play(url: String, position: Int = 0,listener: IMPListener? = null) | |||
| /**播放asset资源 */ | |||
| fun play(asset:String,listener : IMPListener? = null) | |||
| fun playAsset(asset:String, listener : IMPListener? = null) | |||
| /** 暂停播放 ,如果没有播放文件则会忽略*/ | |||
| fun pausePlay() | |||
| @@ -15,8 +15,8 @@ object MPManager:IMP { | |||
| mpUtil.play(url,position,listener) | |||
| } | |||
| override fun play(asset : String, listener : IMPListener?) { | |||
| mpUtil.play(asset, listener) | |||
| override fun playAsset(asset : String, listener : IMPListener?) { | |||
| mpUtil.playAsset(asset, listener) | |||
| } | |||
| override fun pausePlay() { | |||
| @@ -42,6 +42,7 @@ internal class MPUtil : IMP,MediaPlayer.OnPreparedListener,MediaPlayer.OnErrorLi | |||
| } | |||
| override fun play(url: String, position: Int,listener: IMPListener?) { | |||
| isAssetPath = false | |||
| listener?.let { | |||
| this.listener = it | |||
| } | |||
| @@ -61,7 +62,8 @@ internal class MPUtil : IMP,MediaPlayer.OnPreparedListener,MediaPlayer.OnErrorLi | |||
| * @param asset String asset文件名称 | |||
| * @param listener IMPListener? 监听 | |||
| */ | |||
| override fun play(asset : String, listener : IMPListener?) { | |||
| override fun playAsset(asset : String, listener : IMPListener?) { | |||
| isAssetPath = true | |||
| if (asset.isEmpty()) { | |||
| handleError("asset 参数为null ") | |||
| return | |||
| @@ -81,7 +83,7 @@ internal class MPUtil : IMP,MediaPlayer.OnPreparedListener,MediaPlayer.OnErrorLi | |||
| it.mediaPlayer = MediaPlayer() | |||
| } | |||
| mediaPlayer?.apply { | |||
| setOnPreparedListener(mOnPreparedLister) | |||
| setOnPreparedListener(this@MPUtil) | |||
| setOnErrorListener(this@MPUtil) | |||
| setOnCompletionListener(this@MPUtil) | |||
| setOnInfoListener(this@MPUtil) | |||
| @@ -141,10 +143,7 @@ internal class MPUtil : IMP,MediaPlayer.OnPreparedListener,MediaPlayer.OnErrorLi | |||
| } | |||
| } | |||
| } | |||
| private val mOnPreparedLister: MediaPlayer.OnPreparedListener = MediaPlayer.OnPreparedListener { | |||
| } | |||
| /** 异步准备完毕 ,开始播放*/ | |||
| private fun handlePrepared() { | |||
| @@ -158,27 +157,7 @@ internal class MPUtil : IMP,MediaPlayer.OnPreparedListener,MediaPlayer.OnErrorLi | |||
| handleError("状态异常,这里只能是 PREPARED 而现在是:$currentState") | |||
| } | |||
| } | |||
| // private val mOnSeekCompleteListener: MediaPlayer.OnSeekCompleteListener = MediaPlayer.OnSeekCompleteListener { | |||
| // LogUtil.i("mOnSeekCompleteListener 回调") | |||
| // handleSeekComplete() | |||
| // } | |||
| // | |||
| // | |||
| // private val mOnCompleteListener: MediaPlayer.OnCompletionListener = MediaPlayer.OnCompletionListener { | |||
| // handleComplete() | |||
| // } | |||
| // | |||
| // | |||
| // private val mOnErrorListener: MediaPlayer.OnErrorListener = MediaPlayer.OnErrorListener { _, what, extra -> | |||
| // handleError("mOnErrorListener 异常调用 $extra") | |||
| // true | |||
| // } | |||
| // | |||
| // private val mOnInfoListener: MediaPlayer.OnInfoListener = MediaPlayer.OnInfoListener { mp, what, extra -> | |||
| // false | |||
| // } | |||
| private fun handleSeekComplete() { | |||
| when (currentState) { | |||
| EMediaState.PREPARED -> handlePrepared() | |||