| @@ -33,6 +33,10 @@ | |||
| <JetCodeStyleSettings> | |||
| <option name="SPACE_AROUND_RANGE" value="true" /> | |||
| <option name="SPACE_BEFORE_TYPE_COLON" value="true" /> | |||
| <option name="CONTINUATION_INDENT_IN_ARGUMENT_LISTS" value="true" /> | |||
| <option name="CONTINUATION_INDENT_FOR_EXPRESSION_BODIES" value="true" /> | |||
| <option name="CONTINUATION_INDENT_FOR_CHAINED_CALLS" value="true" /> | |||
| <option name="CONTINUATION_INDENT_IN_ELVIS" value="true" /> | |||
| <option name="IF_RPAREN_ON_NEW_LINE" value="false" /> | |||
| <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> | |||
| </JetCodeStyleSettings> | |||
| @@ -178,12 +182,16 @@ | |||
| <option name="RIGHT_MARGIN" value="130" /> | |||
| <option name="KEEP_LINE_BREAKS" value="false" /> | |||
| <option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="true" /> | |||
| <option name="ALIGN_MULTILINE_BINARY_OPERATION" value="true" /> | |||
| <option name="ALIGN_MULTILINE_EXTENDS_LIST" value="true" /> | |||
| <option name="ALIGN_MULTILINE_METHOD_BRACKETS" value="true" /> | |||
| <option name="CALL_PARAMETERS_WRAP" value="1" /> | |||
| <option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="false" /> | |||
| <option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="false" /> | |||
| <option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="false" /> | |||
| <option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="false" /> | |||
| <option name="METHOD_CALL_CHAIN_WRAP" value="5" /> | |||
| <option name="ASSIGNMENT_WRAP" value="0" /> | |||
| <option name="VARIABLE_ANNOTATION_WRAP" value="2" /> | |||
| <option name="ENUM_CONSTANTS_WRAP" value="2" /> | |||
| <option name="WRAP_ON_TYPING" value="0" /> | |||
| @@ -46,7 +46,7 @@ | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_learn_exam.xml" value="0.25" /> | |||
| <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_word.xml" value="0.33" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_learn_word.xml" value="0.5" /> | |||
| <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" /> | |||
| @@ -72,6 +72,7 @@ | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/inc_learn_title.xml" value="0.5784919653893696" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/inc_learn_word.xml" value="0.390625" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/inc_over_number.xml" value="0.4979166666666667" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/inc_spell_learn_tip.xml" value="0.67" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/inc_word.xml" value="0.390625" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/inc_word_detail.xml" value="0.30538922155688625" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/includ_test_option_item.xml" value="0.45153985507246375" /> | |||
| @@ -47,3 +47,5 @@ google 给与的唯一标识符最佳做法: | |||
| 如何实现防快速点击 aspectJ | |||
| SpellTipsLinearLayout onDraw为什么不调用?为什么设置背景后就有效过了? | |||
| @@ -45,7 +45,7 @@ fun IncWordDetailBinding.initValue(phrase : String?, example : String?, refrence | |||
| example?.let { | |||
| tvExampleFlag.visibility = View.VISIBLE | |||
| tvExample.visibility = View.VISIBLE | |||
| tvExample.text = example | |||
| }?:let { | |||
| tvExampleFlag.visibility = View.GONE | |||
| tvExample.visibility = View.GONE | |||
| @@ -54,6 +54,7 @@ fun IncWordDetailBinding.initValue(phrase : String?, example : String?, refrence | |||
| refrence?.let { | |||
| tvReferenceFlag.visibility = View.VISIBLE | |||
| tvReference.visibility = View.VISIBLE | |||
| tvReference.text = refrence | |||
| }?:let { | |||
| tvReferenceFlag.visibility = View.GONE | |||
| tvReference.visibility = View.GONE | |||
| @@ -11,6 +11,7 @@ import com.xkl.cdl.R | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.bean.LearnWord | |||
| import com.xkl.cdl.databinding.ItemHistoricalRouteBinding | |||
| import com.xkl.cdl.util.ViewUtil | |||
| /** | |||
| * author suliang | |||
| @@ -36,7 +37,7 @@ class AdapterHistoricalRoute(val courseType : Int) : BaseRVAdapter<LearnWord>() | |||
| private var previousSelectPosition = -1 | |||
| //是否是历史轨迹最后一个 | |||
| val currentIsFirstSelect : Boolean | |||
| val currentIsLastSelect : Boolean | |||
| get() { | |||
| return currentSelectPosition == itemCount - 1 | |||
| } | |||
| @@ -49,7 +50,8 @@ class AdapterHistoricalRoute(val courseType : Int) : BaseRVAdapter<LearnWord>() | |||
| private var previousIsShowLine = false | |||
| //标记历史记录中最后添加的项是否学习完成 | |||
| private var lastIsLearnOver = false | |||
| var lastIsLearnOver = false | |||
| private set | |||
| override fun coverViewHolder(parent : ViewGroup, viewType : Int) : BaseAdapterViewHolder { | |||
| return BaseAdapterViewHolder(inflateBinding(parent, R.layout.item_historical_route)) | |||
| @@ -70,7 +72,7 @@ class AdapterHistoricalRoute(val courseType : Int) : BaseRVAdapter<LearnWord>() | |||
| //赋值 识字课程只显示括号包裹里面的中文内容 | |||
| tvHistory.run { | |||
| text = when (courseType) { | |||
| AppConstants.COURSE_TYPE_CHINESE_LITERACY -> StringUtil.literacyGetWord(item.word) | |||
| AppConstants.COURSE_TYPE_CHINESE_LITERACY -> ViewUtil.literacyGetWord(item.word) | |||
| else -> item.word | |||
| } | |||
| //设置颜色背景 | |||
| @@ -147,19 +149,23 @@ class AdapterHistoricalRoute(val courseType : Int) : BaseRVAdapter<LearnWord>() | |||
| } | |||
| } | |||
| /**辨音、拼写显示的答案后,轨迹显示值*/ | |||
| /**辨音显示的答案后,轨迹显示值*/ | |||
| fun showHistoricalRouteValue(){ | |||
| currentIsShowLine = false | |||
| notifyItemChanged(currentSelectPosition) | |||
| } | |||
| /** 当前项学习完成 ,判断标记最后一项是否学习完成*/ | |||
| fun currentLearnOver(){ | |||
| if (currentSelectPosition == itemCount -1 ) lastIsLearnOver = true | |||
| } | |||
| /** 拼写错误,需要在点击下一条时,移除当前item项 */ | |||
| fun spellLastLearnError(){ | |||
| removeData(currentSelectPosition) | |||
| /** 拼写完成,点击下一条时,按需要调用移除当前item项 */ | |||
| fun spellNeedRemoveCurrent(){ | |||
| val tempPosition = currentSelectPosition | |||
| currentIsShowLine = false | |||
| currentSelectPosition = -1 | |||
| removeData(tempPosition) | |||
| } | |||
| @@ -172,7 +178,7 @@ class AdapterHistoricalRoute(val courseType : Int) : BaseRVAdapter<LearnWord>() | |||
| super.setData(data) | |||
| } | |||
| override fun addData(data : LearnWord?, position : Int?) { | |||
| override fun addData(data : LearnWord?, position : Int? ) { | |||
| previousSelectPosition = currentSelectPosition | |||
| currentSelectPosition = position ?: itemCount | |||
| //添加数据拼写辨音初始值 | |||
| @@ -29,19 +29,22 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| //当前状态 是否拼写中 | |||
| var isSpelling = true | |||
| @SuppressLint("NotifyDataSetChanged") set(value) { | |||
| field = value | |||
| //初始设置,设置数据的时候就已经将值修改为true,会设置数据的进行notify了 | |||
| if (!value) notifyDataSetChanged() | |||
| } | |||
| private set | |||
| //纠错矫正 | |||
| var isErrorRecovery = false | |||
| @SuppressLint("NotifyDataSetChanged") set(value) { | |||
| field = value | |||
| //设置数据时默认为false ,只有后面纠错的时候改为true后才需要全更新 | |||
| if (value) notifyDataSetChanged() | |||
| } | |||
| private var isErrorRecovery = false | |||
| /** 打开纠错点击 */ | |||
| fun openErrorRecovery() { | |||
| isSpelling = false | |||
| isErrorRecovery = true | |||
| } | |||
| /** 关闭纠错点击 */ | |||
| fun closeErrorRecovery() { | |||
| isErrorRecovery = false | |||
| } | |||
| /** 初始设置数据,则将拼写状态修改为true */ | |||
| override fun setData(data : MutableList<SpellItemBean>?) { | |||
| @@ -78,59 +81,55 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| val item = getItem(position) | |||
| val binding = holder.binding as ItemSpellSingleWordBinding | |||
| binding.tv.text = item.char.toString() | |||
| if (isSpelling) { //可拼写点击时,选中灰色,未选中白色 | |||
| binding.tv.setBackgroundColor(ContextCompat.getColor(context, if (item.isSelected) R.color.gray_1 else R.color.white)) | |||
| //点击 | |||
| binding.tv.setOnClickListener { | |||
| if (!isSpellingItemCouldClick(position)) { //不可点击 | |||
| clickInvalid() | |||
| } else { //可点击 | |||
| playItem(item.char) | |||
| item.isSelected = true | |||
| getItem(if (position % 2 == 0) position + 1 else position - 1).isSelected = false | |||
| notifyUIUpdate(position) | |||
| notifyDataSetChanged() | |||
| binding.tv.setBackgroundColor(when { | |||
| isSpelling -> ContextCompat.getColor(context, | |||
| if (item.isSelected) R.color.gray_1 else R.color.white) //可拼写点击时,选中灰色,未选中白色 | |||
| else -> ContextCompat.getColor(context, | |||
| if (item.isCorrect && !item.isSelected) R.color.red_1 else R.color.white) //不可拼写时 正确项未选中为红色,否则为白色 | |||
| }) | |||
| binding.tv.setOnClickListener { | |||
| when { | |||
| //拼写 | |||
| isSpelling -> when { | |||
| //可点击 | |||
| isSpellingItemCouldClick(position) -> { | |||
| playItem(item.char) | |||
| item.isSelected = true | |||
| val otherLinePosition = if (position % 2 == 0) position + 1 else position - 1 | |||
| getItem(otherLinePosition).isSelected = false | |||
| //是否拼写完成 | |||
| val isOver = position == itemCount - 1 || position == itemCount - 2 | |||
| if (isOver) { | |||
| isSpelling = false | |||
| notifyDataSetChanged() | |||
| } else { | |||
| notifyItemChanged(position) | |||
| notifyItemChanged(otherLinePosition) | |||
| } | |||
| notifySpellData(isOver, position) | |||
| } | |||
| //不可点击 | |||
| else -> clickInvalid() | |||
| } | |||
| } | |||
| } else { //不可拼写时 正确项未选中为红色,否则为白色 | |||
| binding.tv.setBackgroundColor(ContextCompat.getColor(context, | |||
| if (item.isCorrect && !item.isSelected) R.color.red_1 else R.color.white)) | |||
| binding.tv.setOnClickListener { | |||
| //非纠错校正则点击无效 | |||
| if (!isErrorRecovery) return@setOnClickListener | |||
| //校正点击需要从前向后,先判断前面是否有未点击的,有未点击则判断为不可点击 | |||
| if (!isRecoveryItemCouldClick(position)) { | |||
| clickInvalid() | |||
| } else { | |||
| playItem(item.char) | |||
| item.isSelected = true | |||
| notifyUIUpdate(position) | |||
| notifyItemChanged(position) | |||
| //纠错 | |||
| isErrorRecovery -> when { | |||
| //可点击 | |||
| isRecoveryItemCouldClick(position) -> { | |||
| playItem(item.char) | |||
| item.isSelected = true | |||
| notifyItemChanged(position) | |||
| notifyRecoveryData(position) | |||
| } | |||
| //不可点击 | |||
| else -> clickInvalid() | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /**点击无效 | |||
| * 提示,播放mistake音 | |||
| */ | |||
| private fun clickInvalid() { | |||
| Toast.makeText(context, "请依次点击", Toast.LENGTH_SHORT).show() | |||
| MPManager.playAsset("common_voice/mistake.mp3") | |||
| } | |||
| /** item 播放 */ | |||
| private fun playItem(letter : Char) { | |||
| if (letter.isLetter()) { | |||
| when (defaultSoundWay) { | |||
| 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.playAsset("common_voice/konge.mp3") | |||
| } | |||
| } | |||
| /** | |||
| * 当前item是否可点击 | |||
| * @param position Int item中的位置 | |||
| @@ -152,110 +151,109 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| * @param position Int item中的位置 | |||
| */ | |||
| private fun isRecoveryItemCouldClick(position : Int) : Boolean { | |||
| //第一列 position 为 0 2 4 6 8 | |||
| //第二列 position为 1 3 5 7 9 | |||
| if (position == 0 || position == 1) return true | |||
| //本列之前,即上一列的第二行是否有正确项未被点击 | |||
| val previousColumnSecondPosition = if (position % 2 == 0) position - 1 else position - 2 | |||
| (0 .. previousColumnSecondPosition).forEach { | |||
| val item = getItem(it) | |||
| //正确项但未被选中 | |||
| if (item.isCorrect && !item.isSelected) { | |||
| return false | |||
| var result = true | |||
| val item1 = getItem(position) | |||
| //本项未选中,且正确,才需要判断修改 | |||
| if (!item1.isSelected && item1.isCorrect) { | |||
| for (i in 0 until position) { | |||
| val item = getItem(i) | |||
| //正确项但未被选中 | |||
| if (item.isCorrect && !item.isSelected) { | |||
| result = false | |||
| break | |||
| } | |||
| } | |||
| }else{ | |||
| result = false | |||
| } | |||
| return true | |||
| return result | |||
| } | |||
| /** | |||
| * 获取拼写字用于去更新ui | |||
| * @param position Int 点击项 | |||
| * @return SpannableStringBuilder 拼写的值 | |||
| * 拼写中赋值 | |||
| * @param isOver 是否是最后一列,即判断拼写是否完成 | |||
| * @param position 点击的位置 | |||
| */ | |||
| private fun notifyUIUpdate(position : Int) { | |||
| private fun notifySpellData(isOver : Boolean, position : Int) { | |||
| //记录拼写的值 | |||
| val builder = SpannableStringBuilder() | |||
| if (isSpelling) { | |||
| //是否是最后一列 | |||
| val isOver = (position == itemCount - 1) || (position == itemCount - 2) | |||
| var errorSize = 0 | |||
| var nextPosition = 0 //下一列的第一行坐标位置 | |||
| //记录正确的值,如果没有选中,则需要设置颜色 | |||
| val correctValue = SpannableStringBuilder() | |||
| if (!isOver) { | |||
| //拼写时,直接赋值 | |||
| getData().forEachIndexed { index, it -> | |||
| if (it.isSelected) builder.append(it.char) | |||
| //滑动到下一列的位置 | |||
| if (position == index) { //点击的位置 | |||
| if (position % 2 == 0) { | |||
| nextPosition = position + 2 | |||
| } else { | |||
| nextPosition = position + 1 | |||
| //记录正确的值,如果没有选中,则需要设置颜色 | |||
| val correctValue = SpannableStringBuilder() | |||
| var errorSize = 0 | |||
| var nextPosition = 0 //下一列的第一行坐标位置 | |||
| when { | |||
| //拼写完成 | |||
| isOver -> 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 | |||
| } | |||
| } | |||
| } | |||
| } else { //拼写完成 | |||
| 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 | |||
| } | |||
| } | |||
| } | |||
| //拼写未完成时,直接赋值 | |||
| else -> getData().forEachIndexed { index, it -> | |||
| if (it.isSelected) builder.append(it.char) | |||
| //滑动到下一列的位置 | |||
| if (position == index) { //点击的位置 | |||
| if (position % 2 == 0) { | |||
| nextPosition = position + 2 | |||
| } else { | |||
| nextPosition = position + 1 | |||
| } | |||
| } | |||
| isSpelling = false | |||
| } | |||
| //回调出去 | |||
| onItemSpellingClickListener(builder, nextPosition, isOver, correctValue, errorSize) | |||
| } | |||
| //纠错: 拼接所有正确的内容,未选中的需要设置颜色 | |||
| if (isErrorRecovery) { | |||
| //是否完成的标记 | |||
| var isOver = true | |||
| //下一个错误的位置 | |||
| var nextPosition = 0 | |||
| //当前列的第二行坐标 | |||
| val currentSecondPosition = if (position % 2 == 0) position + 1 else position | |||
| getData().forEachIndexed { index, it -> | |||
| when { | |||
| it.isCorrect -> when { | |||
| it.isSelected -> builder.append(it.char) | |||
| else -> { | |||
| builder.append(SpannableString(it.char.toString()).apply { | |||
| setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.theme_color)), | |||
| 0, | |||
| 1, | |||
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) | |||
| }) | |||
| //判断是否还有大于当前列第二行坐标的未选,如果有,则为纠错未完成 | |||
| if (index > currentSecondPosition) { | |||
| isOver = false | |||
| nextPosition = index | |||
| } | |||
| //回调出去 | |||
| onItemSpellingClickListener(builder, nextPosition, isOver, correctValue, errorSize) | |||
| } | |||
| /**纠错中赋值 拼接所有正确的内容,未选中的需要设置颜色 | |||
| * @param position Int 点击位置 | |||
| * @return Boolean 是否纠错完成 | |||
| */ | |||
| private fun notifyRecoveryData(position : Int) { | |||
| //记录纠错后显示的值 | |||
| val builder = SpannableStringBuilder() | |||
| //是否完成的标记 | |||
| var isOver = true | |||
| //下一个错误的位置 | |||
| var nextPosition = 0 | |||
| getData().forEachIndexed { index, it -> | |||
| when { | |||
| it.isCorrect -> when { | |||
| it.isSelected -> builder.append(it.char) | |||
| else -> { | |||
| builder.append(SpannableString(it.char.toString()).apply { | |||
| setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.theme_color)), 0, 1, | |||
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) | |||
| }) | |||
| //判断是否还有大于当前坐标的未选,如果有,则为纠错未完成 | |||
| if (nextPosition == 0 && index > position) { | |||
| isOver = false | |||
| nextPosition = index | |||
| } | |||
| } | |||
| } | |||
| } | |||
| onItemRecoveryClick(builder, isOver, nextPosition) | |||
| } | |||
| if (isOver) isErrorRecovery = false | |||
| onItemRecoveryClick(builder, isOver, nextPosition) | |||
| } | |||
| /** 测试时倒计时结束,进行取值结果回调 */ | |||
| @SuppressLint("NotifyDataSetChanged") | |||
| fun testCountingTimeOver() { | |||
| val builder = SpannableStringBuilder() | |||
| val correctValue = SpannableStringBuilder() | |||
| @@ -269,9 +267,7 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| 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, | |||
| setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.theme_color)), 0, 1, | |||
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) | |||
| }) | |||
| //错误数加1 | |||
| @@ -284,4 +280,25 @@ class AdapterSpell : BaseRVAdapter<SpellItemBean>() { | |||
| onItemSpellingClickListener(builder, 0, true, correctValue, errorSize) | |||
| notifyDataSetChanged() | |||
| } | |||
| /**点击无效 | |||
| * 提示,播放mistake音 | |||
| */ | |||
| private fun clickInvalid() { | |||
| Toast.makeText(context, "请依次点击", Toast.LENGTH_SHORT).show() | |||
| MPManager.playAsset("common_voice/mistake.mp3") | |||
| } | |||
| /** item 播放 */ | |||
| private fun playItem(letter : Char) { | |||
| if (letter.isLetter()) { | |||
| when (defaultSoundWay) { | |||
| 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.playAsset("common_voice/konge.mp3") | |||
| } | |||
| } | |||
| } | |||
| @@ -171,18 +171,20 @@ object AppConstants { | |||
| 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 DATA_LESSON_LEARN_OVER = 9 | |||
| /**--- 弹窗动作 --------------------------------- */ | |||
| /** 学前总测结束弹窗: 开始学习 ,课时学前测试开始弹窗*/ | |||
| const val DIALOG_START_LEARN = 1 | |||
| /** 弹窗按钮动作:开始测试 */ | |||
| const val DIALOG_START_TEST = 2 | |||
| /**课时学后测试弹窗动作: 重新学习*/ | |||
| const val DIALOG_LESSON_AFTER_TEST_RELEARN = 3 | |||
| /**课时学习结束与学后测试弹窗动作: 重新学习*/ | |||
| const val DIALOG_LESSON_RELEARN = 3 | |||
| /**课时学后测试弹窗动作: 再测一次*/ | |||
| const val DIALOG_LESSON_AFTER_TEST_AGAIN = 4 | |||
| /**课时学后测试弹窗动作: 下一步*/ | |||
| @@ -1,6 +1,7 @@ | |||
| package com.xkl.cdl.data.bean | |||
| import com.xkl.cdl.R | |||
| import java.io.Serializable | |||
| /** | |||
| * 数据基类 | |||
| @@ -30,7 +31,7 @@ open class BaseWord(val subjectId: Int, | |||
| val lessonId: Long, | |||
| val wordId: Long, | |||
| var first: Boolean, | |||
| val lessonType: Int) { | |||
| val lessonType: Int) : Serializable { | |||
| /** 大复习次数 */ | |||
| var reviewNum: Int = 0 | |||
| @@ -1,7 +1,5 @@ | |||
| package com.xkl.cdl.data.bean | |||
| import com.xkl.cdl.data.bean.BaseWord | |||
| /** | |||
| * author suliang | |||
| * create 2022/3/31 17:52 | |||
| @@ -32,7 +30,6 @@ class LearnWord(subjectId: Int, | |||
| var phonetic_uk: String? = null // 英式音标 | |||
| var phonetic_us: String? = null // 美式音标 | |||
| var phonetic_cn: String? = null // 中文拼音 | |||
| // var photo: ByteArray? = null // 图片 | |||
| var basic_explanation: String? = null //基本释义 | |||
| var extend_explanation: String? = null //扩展释义 | |||
| var phrase: String? = null //词组 | |||
| @@ -9,6 +9,9 @@ import com.xkl.cdl.data.bean.course.Lesson | |||
| * Describe: 课时学习时的传参 | |||
| */ | |||
| class LearnData(val lesson: Lesson) { | |||
| /**学前总、学前测试错误集合, 仅用于学习 */ | |||
| var examErrorMap: HashMap<String,Boolean>? = null | |||
| /**学习数据*/ | |||
| var learnWordList : List<LearnWord> = mutableListOf() | |||
| @@ -18,4 +18,6 @@ class LearnEventData(val subjectId : Int, var courseId : Long, val actionFlag : | |||
| //课时学前测试 除了学前总测传递数据,还需要课时位置数据, | |||
| var leesonPositionIndex = 0 // lesson所在位置 | |||
| //学习结束 newErrorMap 保存为所有的错误,包含学前和课程前的测试,主要用与后面进行小游戏的数据加载 | |||
| } | |||
| @@ -1,10 +1,11 @@ | |||
| package com.xkl.cdl.data.manager | |||
| import android.util.Size | |||
| import appApi.AppApi | |||
| import com.suliang.common.util.file.FileUtil | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.bean.course.CoursePack | |||
| import com.xkl.cdl.data.bean.course.ExamBean | |||
| import com.xkl.cdl.data.bean.course.Lesson | |||
| import java.io.File | |||
| import java.math.BigDecimal | |||
| import java.math.RoundingMode | |||
| @@ -20,6 +21,10 @@ object CourseManager { | |||
| /** 项目对应的课程包 */ | |||
| val subjectWithCoursePackMap = hashMapOf<Int, List<CoursePack>>() | |||
| /** 所有课程进度 key:项目id value:该项目下的所有课程的进度信息,value仅包含为有了学习时长的课程*/ | |||
| val mSortInfoList = hashMapOf<Int, MutableList<AppApi.CourseSortedInfo.Builder>>() | |||
| /** | |||
| * 获取对应项目的课程数量 | |||
| * @param subjectId Int 项目 | |||
| @@ -29,12 +34,10 @@ object CourseManager { | |||
| return subjectWithCoursePackMap[subjectId]?.let { | |||
| var count = 0 | |||
| it.forEach { coursePack -> | |||
| count += coursePack.childrenCourses?.size | |||
| ?: 0 | |||
| count += coursePack.childrenCourses?.size ?: 0 | |||
| } | |||
| count | |||
| } | |||
| ?: 0 | |||
| } ?: 0 | |||
| } | |||
| /** | |||
| @@ -83,9 +86,8 @@ object CourseManager { | |||
| subjectWithCoursePackMap.forEach { entry -> | |||
| entry.value.forEach { coursePack -> | |||
| coursePack.childrenCourses.forEach { | |||
| val file = File( | |||
| FileUtil.getSaveDirPath("db"), "${entry.key}/${coursePack.coursePackId}/${it.courseId}/course.db" | |||
| ) | |||
| val file = File(FileUtil.getSaveDirPath("db"), | |||
| "${entry.key}/${coursePack.coursePackId}/${it.courseId}/course.db") | |||
| //不存在,复制,存在则不操作 | |||
| if (!file.exists()) { | |||
| FileUtil.copyAsset(it.dbPathName, file) | |||
| @@ -133,7 +135,7 @@ object CourseManager { | |||
| }.sum() * 1.6 / 60)).toInt()) | |||
| } | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPOKEN -> { | |||
| when(testType) { | |||
| when (testType) { | |||
| AppConstants.TEST_TYPE_BEFORE_TOTAL, AppConstants.TEST_TYPE_AFTER_TOTAL -> { | |||
| "共${testData.size}题" | |||
| } | |||
| @@ -156,10 +158,10 @@ object CourseManager { | |||
| * @param examType Int 测试类型 | |||
| * @return String 测试类型名称 | |||
| */ | |||
| fun getExamTypeName(examType: Int): String{ | |||
| return when(examType){ | |||
| fun getExamTypeName(examType : Int) : String { | |||
| return when (examType) { | |||
| AppConstants.TEST_TYPE_BEFORE_TOTAL -> "学前总测试" | |||
| AppConstants.TEST_TYPE_BEFORE -> "课时学前测试" | |||
| AppConstants.TEST_TYPE_BEFORE -> "课时学前测试" | |||
| AppConstants.TEST_TYPE_AFTER -> "课时学后测试" | |||
| AppConstants.TEST_TYPE_AFTER_TOTAL -> "学后总测试" | |||
| AppConstants.TEST_TYPE_MEMO -> "备忘本测试" | |||
| @@ -169,4 +171,122 @@ object CourseManager { | |||
| else -> "" | |||
| } | |||
| } | |||
| /** | |||
| * 英语类型课程进度计算 | |||
| * @param allLesson List<Lesson> 所有课时 | |||
| * @param isRelearn Boolean 是否重学课时的计算 | |||
| * @param relearnLessonPosition Int 需要重学的课时在集合中的位置 | |||
| */ | |||
| fun calculateEnglishCourseProgress(allLesson : List<Lesson>, | |||
| isRelearn : Boolean = false, | |||
| relearnLessonPosition : Int = -1) : Double { | |||
| //总数 | |||
| var totalWordSize = 0.0 | |||
| //已学数 | |||
| var totalLearnSize = 0 | |||
| allLesson.forEachIndexed { index, lesson -> | |||
| //总数 | |||
| totalWordSize += lesson.totalNumber | |||
| if (isRelearn && index == relearnLessonPosition) { | |||
| return@forEachIndexed | |||
| } | |||
| totalLearnSize += lesson.learnedIndex + 1 | |||
| } | |||
| return totalLearnSize / totalWordSize * 100 | |||
| } | |||
| /** | |||
| * 作文课程进度计算 | |||
| * @param allLesson List<Lesson> 所有课时 | |||
| * @param isRelearn Boolean 是否重学课时的计算 | |||
| * @param relearnLessonPosition Int 需要重学的课时在集合中的位置 | |||
| */ | |||
| fun calculateCompositionCourseProgess(allLesson : List<Lesson>, | |||
| isRelearn : Boolean = false, | |||
| relearnLessonPosition : Int = -1) : Double { | |||
| // TODO: 2022/4/29 计算作文课程的进度 | |||
| return 0.0 | |||
| } | |||
| /** | |||
| * 口语课程的进度计算 | |||
| * @param allLesson List<Lesson> | |||
| * @param isRelearn Boolean | |||
| * @param relearnLessonPosition Int | |||
| * @return Double | |||
| */ | |||
| fun calculateSpokenCourseProgress(allLesson : List<Lesson>, | |||
| isRelearn : Boolean = false, | |||
| relearnLessonPosition : Int = -1):Double{ | |||
| // TODO: 2022/4/29 计算口语课程的进度 | |||
| return 0.0 | |||
| } | |||
| /** | |||
| * 计算项目总进度 | |||
| * @param subjectId Int 项目Id | |||
| * @param packId Long 修改进度的课程包id | |||
| * @param courseId Long 修改进度的课程id | |||
| * @param courseProgress Double 修改了的课程进度 | |||
| * @return Double 项目总进度 | |||
| */ | |||
| fun calculateSubjectProgress(subjectId : Int, packId : Long, courseId : Long, courseProgress : Double) : Double { | |||
| var totalProgress = 0.0 | |||
| //没有信息则进行添加进入集合,有信息则修改进度 | |||
| mSortInfoList.get(subjectId)?.let { | |||
| it.forEach { | |||
| if (it.packId == packId && it.courseId == courseId) { | |||
| it.s = courseProgress | |||
| } | |||
| totalProgress += it.s | |||
| } | |||
| } ?: let { | |||
| val newCourseSortedInfoBuilder = AppApi.CourseSortedInfo.newBuilder().apply { | |||
| this.packId = packId | |||
| this.courseId = courseId | |||
| s = courseProgress | |||
| totalProgress += this.s | |||
| } | |||
| mSortInfoList.put(subjectId, mutableListOf(newCourseSortedInfoBuilder)) | |||
| } | |||
| if (totalProgress == 0.0 ) return totalProgress | |||
| var totalCourseSize = 0 | |||
| subjectWithCoursePackMap.get(subjectId)?.forEach { | |||
| totalCourseSize += it.childrenCourses.size | |||
| } | |||
| if (totalCourseSize == 0) return 0.0 | |||
| return totalProgress / totalCourseSize | |||
| } | |||
| /** | |||
| * 重学课程时,计算项目的进度 | |||
| * @param subjectId Int 项目 | |||
| * @param courseId Long 重学的课程 | |||
| * @return Double 项目的总进度 | |||
| */ | |||
| fun calculateSubjectProgressWithCourseRelearn(subjectId : Int,coursePackId:Long, courseId : Long) : Double { | |||
| val totalProgress = mSortInfoList.get(subjectId)?.let { | |||
| var tempProgress = 0.0 | |||
| it.forEach { | |||
| if (it.packId == coursePackId && it.courseId == courseId){ | |||
| return@forEach | |||
| } | |||
| tempProgress += it.s | |||
| } | |||
| tempProgress | |||
| }?: 0.0 | |||
| if (totalProgress == 0.0 ) return totalProgress | |||
| var totalCourseSize = 0 | |||
| subjectWithCoursePackMap.get(subjectId)?.forEach { | |||
| totalCourseSize += it.childrenCourses.size | |||
| } | |||
| if (totalCourseSize == 0) return 0.0 | |||
| return totalProgress / totalCourseSize | |||
| } | |||
| } | |||
| @@ -1,10 +1,12 @@ | |||
| package com.xkl.cdl.data.repository | |||
| import androidx.lifecycle.MutableLiveData | |||
| import com.xkl.cdl.data.bean.course.CourseDetail | |||
| import com.xkl.cdl.data.bean.course.Lesson | |||
| import com.xkl.cdl.data.manager.db.DBCourseManager | |||
| import com.xkl.cdl.data.manager.db.DbControlBase | |||
| import io.reactivex.rxjava3.core.Observable | |||
| import mqComsumerV1.Struct | |||
| /** | |||
| * author suliang | |||
| @@ -46,6 +48,50 @@ object DataRepository { | |||
| it.onComplete() | |||
| } | |||
| } | |||
| /** | |||
| * 保存Struct.Record数据 | |||
| * @param record Builder | |||
| * @return Boolean | |||
| */ | |||
| fun saveRecord(record : Struct.Record.Builder): Boolean { | |||
| var subjectId : Long = 0 | |||
| var coursePackId : Long = 0 | |||
| var courseId : Long = 0 | |||
| when{ | |||
| record.entityCount > 0 -> { | |||
| record.getEntity(0).let { | |||
| subjectId = it.projectId | |||
| coursePackId = it.packId | |||
| courseId = it.courseId | |||
| } | |||
| } | |||
| record.examCount > 0 -> { | |||
| record.getExam(0).let { | |||
| subjectId = it.projectId | |||
| coursePackId = it.packId | |||
| courseId = it.courseId | |||
| } | |||
| } | |||
| record.durationCount > 0 -> { | |||
| record.getDuration(0).let { | |||
| subjectId = it.projectId | |||
| coursePackId = it.packId | |||
| courseId = it.courseId | |||
| } | |||
| } | |||
| } | |||
| if (subjectId != 0L ){ | |||
| //计算词汇量和学习效率 | |||
| calcCourseVocabularyAndEfficiency(subjectId,coursePackId,courseId) | |||
| } | |||
| // TODO: 2022/4/29 调用保存方法 | |||
| return true | |||
| } | |||
| /** | |||
| * 计算当前词汇量 与 学习效率 | |||
| */ | |||
| public fun calcCourseVocabularyAndEfficiency( projectId:Long, packId:Long, courseId:Long) { | |||
| } | |||
| } | |||
| @@ -7,12 +7,12 @@ 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.LogUtil | |||
| import com.suliang.common.util.os.ScreenUtil | |||
| import com.xkl.cdl.R | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.bean.LearnDialogBean | |||
| import com.xkl.cdl.databinding.DialogLessonLearnBinding | |||
| import kotlin.random.Random | |||
| /** | |||
| * author suliang | |||
| @@ -61,9 +61,7 @@ class LearnDialog private constructor() : BaseDialogFragment<DialogLessonLearnBi | |||
| AppConstants.TEST_TYPE_BEFORE-> initLessonBeforeTestOver() | |||
| } | |||
| //学习结束弹窗 | |||
| AppConstants.DIALOG_TYPE_LEARN_OVER -> { | |||
| } | |||
| AppConstants.DIALOG_TYPE_LEARN_OVER -> initLessonLearningOver() | |||
| } | |||
| } | |||
| @@ -179,7 +177,31 @@ class LearnDialog private constructor() : BaseDialogFragment<DialogLessonLearnBi | |||
| } | |||
| /** | |||
| * 课时测试结束 | |||
| * 课时学习中的学习完成 | |||
| */ | |||
| private fun initLessonLearningOver(){ | |||
| initNumber() | |||
| binding.run { | |||
| imgIv.setImageResource(if (Random.nextBoolean()) R.mipmap.boy_2 else R.mipmap.girl_2) | |||
| tvTitle.visibility = View.VISIBLE | |||
| tvTitle.text = "恭喜你,本课时学习完成" | |||
| incStatisticsNumber.root.visibility = View.VISIBLE | |||
| tvLearnOverTip.visibility = View.VISIBLE | |||
| tvLearnOverForAfterCountTime.visibility = View.VISIBLE | |||
| tvLearnOverForAfterCountTime.text = learnDialogBean.showTimeCount | |||
| tvLeft.visibility = View.VISIBLE | |||
| tvLeft.text = "重新学习" | |||
| vSplit.visibility = View.VISIBLE | |||
| tvRight.text = "开始测试" | |||
| } | |||
| binding.tvLeft.click { onDialogListener(AppConstants.DIALOG_LESSON_RELEARN, this) } | |||
| binding.tvRight.click { onDialogListener(AppConstants.DIALOG_START_TEST, this) } | |||
| } | |||
| /** | |||
| * 课时后测试结束 | |||
| */ | |||
| private fun initLessonAfterTestOver(){ | |||
| initNumber() | |||
| @@ -205,7 +227,7 @@ class LearnDialog private constructor() : BaseDialogFragment<DialogLessonLearnBi | |||
| } | |||
| binding.tvTop.click { | |||
| onDialogListener(AppConstants.DIALOG_LESSON_AFTER_TEST_RELEARN,this) | |||
| onDialogListener(AppConstants.DIALOG_LESSON_RELEARN, this) | |||
| } | |||
| binding.tvLeft.click { | |||
| onDialogListener(AppConstants.DIALOG_LESSON_AFTER_TEST_AGAIN,this) | |||
| @@ -14,33 +14,59 @@ import kotlin.concurrent.timerTask | |||
| * create 2022/4/2 14:06 | |||
| * Describe: 总计时的实现 | |||
| */ | |||
| abstract class LearnBaseViewModel : BaseViewModel() { | |||
| abstract class LearnBaseViewModel : BaseViewModel() { | |||
| //标记是否测试完成 | |||
| var isAllOver = false | |||
| protected var mHandler = Handler(Looper.getMainLooper()) | |||
| //记录总消耗时间 | |||
| var totalUseTime : MutableLiveData<Long> = MutableLiveData(0) | |||
| val totalUseTime : MutableLiveData<Long> = MutableLiveData(0) | |||
| private var totalTimer : Timer = Timer() | |||
| private var timeTask : TimerTask? = null | |||
| //是否进行计时,默认计时,为false不计时,更改为false的条件时,出现弹窗或者当前页不在前台 | |||
| // var countingEnable = true | |||
| /** 学习的有效计时 */ | |||
| var isRunValidTime = true | |||
| /**有效时间为18秒*/ | |||
| private val validMaxTime = 18000 | |||
| /**记录有效时间*/ | |||
| val validTime : MutableLiveData<Long> = MutableLiveData(0) | |||
| /**记录当前倒计时已运行时间 默认18秒 */ | |||
| private var currentValidSurplusTime = validMaxTime | |||
| /** 开始计时 */ | |||
| fun startTotalCounting(){ | |||
| fun startTotalCounting() { | |||
| timeTask = timerTask { | |||
| totalUseTime.postValue(totalUseTime.value?.plus(200)) | |||
| totalUseTime.postValue(totalUseTime.value?.plus(200)) | |||
| //如果需要记录有效时间 | |||
| if (isRunValidTime) { | |||
| validTime.postValue(validTime.value!!.plus(200)) | |||
| currentValidSurplusTime -= 200 | |||
| isRunValidTime = currentValidSurplusTime > 0 | |||
| } | |||
| } | |||
| totalTimer.schedule(timeTask, 200, 200) | |||
| LogUtil.e("开始总计时") | |||
| } | |||
| /** 停止计时 */ | |||
| fun stopTotalCountTing(){ | |||
| fun stopTotalCountTing() { | |||
| LogUtil.e("停止总计时") | |||
| timeTask?.cancel() | |||
| timeTask = null | |||
| } | |||
| /**执行有效计时*/ | |||
| fun executeLearnValidTime(){ | |||
| if(isAllOver) return | |||
| currentValidSurplusTime = validMaxTime | |||
| isRunValidTime = true | |||
| } | |||
| } | |||
| @@ -166,8 +166,7 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| } | |||
| else -> { | |||
| //默认发音 | |||
| vm.defaultSoundWay = UserInfoManager.getDefaultSoundWay() | |||
| voiceSwitch.setSoundWay(vm.defaultSoundWay) | |||
| voiceSwitch.setSoundWay(UserInfoManager.getDefaultSoundWay()) | |||
| voiceSwitch.soundWayChange.observe(this@LearnExamActivity) { | |||
| vm.defaultSoundWay = it | |||
| UserInfoManager.putDefaultSoundWay(it) | |||
| @@ -630,7 +629,7 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| //课时学后测试结束弹窗动作 | |||
| AppConstants.TEST_TYPE_AFTER -> when(action){ | |||
| //重新学习 | |||
| AppConstants.DIALOG_LESSON_AFTER_TEST_RELEARN -> { | |||
| AppConstants.DIALOG_LESSON_RELEARN -> { | |||
| CommonDialog.newInstance(CommonDialogBean(titleText = R.string.lesson_relearn_title, | |||
| contentText = R.string.lesson_relearn_content, | |||
| leftText = R.string.cancel, | |||
| @@ -17,6 +17,7 @@ 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.data.repository.DataRepository | |||
| import com.xkl.cdl.databinding.IncludTestOptionItemBinding | |||
| import io.reactivex.rxjava3.core.Observable | |||
| import kotlinx.coroutines.delay | |||
| @@ -627,12 +628,13 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| showHideLoading(true) | |||
| Observable.create<Boolean> { | |||
| viewModelScope.launch { | |||
| delay(2000) | |||
| delay(1000) | |||
| it.onNext(true) | |||
| } | |||
| // TODO: 2022/4/14 传递保存record信息 | |||
| DataRepository.saveRecord(record) | |||
| // record 已经实例化并已经将数据保存 | |||
| LogUtil.e(JsonFormat.printToString(record.build())) | |||
| // LogUtil.e(JsonFormat.printToString(record.build())) | |||
| }.compose(diskIo2Main()).subscribe({ | |||
| showHideLoading(false) | |||
| @@ -679,9 +681,7 @@ class LearnExamViewModel : LearnBaseViewModel() { | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,32 +1,44 @@ | |||
| package com.xkl.cdl.module.learn | |||
| import android.content.Context | |||
| import android.annotation.SuppressLint | |||
| import android.os.Bundle | |||
| import android.text.SpannableStringBuilder | |||
| import android.text.TextUtils | |||
| import android.util.TypedValue | |||
| import android.view.MotionEvent | |||
| import android.view.View | |||
| import android.widget.LinearLayout | |||
| import android.widget.TextView | |||
| import androidx.core.content.ContextCompat | |||
| import androidx.lifecycle.ViewModelProvider | |||
| import androidx.recyclerview.widget.GridLayoutManager | |||
| import androidx.recyclerview.widget.LinearLayoutManager | |||
| import androidx.recyclerview.widget.RecyclerView | |||
| import androidx.recyclerview.widget.RecyclerView.OnScrollListener | |||
| import androidx.recyclerview.widget.SimpleItemAnimator | |||
| 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.LogUtil | |||
| import com.suliang.common.util.image.ImageLoader | |||
| 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.AdapterHistoricalRoute | |||
| import com.xkl.cdl.adapter.AdapterSpell | |||
| import com.xkl.cdl.adapter.itemdecoration.SpellItemDecoration | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.bean.LearnDialogBean | |||
| import com.xkl.cdl.data.bean.LearnWord | |||
| import com.xkl.cdl.data.event.LearnEventData | |||
| import com.xkl.cdl.data.manager.UserInfoManager | |||
| import com.xkl.cdl.databinding.ActivityLearnWordBinding | |||
| import com.xkl.cdl.databinding.IncLearnSpellBinding | |||
| import com.xkl.cdl.databinding.IncLearnWordBinding | |||
| import com.xkl.cdl.databinding.IncWordDetailBinding | |||
| import com.xkl.cdl.data.repository.AudioCache | |||
| import com.xkl.cdl.data.repository.PhotoCache | |||
| import com.xkl.cdl.databinding.* | |||
| import com.xkl.cdl.dialog.CommonDialog | |||
| import com.xkl.cdl.dialog.CommonDialogBean | |||
| import com.xkl.cdl.dialog.LearnDialog | |||
| import com.xkl.cdl.initValue | |||
| import com.xkl.cdl.util.ViewUtil | |||
| /** | |||
| * 正常的学习、复习、自动播放 | |||
| @@ -42,8 +54,13 @@ class LearnWordActivity : BaseActivityVM<ActivityLearnWordBinding, LearnWordView | |||
| //认读、辨音的内容布局 | |||
| private lateinit var bindingWord : IncLearnWordBinding | |||
| //详情布局 | |||
| private lateinit var incWorcDetailBinding: IncWordDetailBinding | |||
| //拼写内容布局 | |||
| private lateinit var bindingSpell : IncLearnSpellBinding | |||
| //拼写前的提示 | |||
| private lateinit var incSpellTipBinding : IncSpellLearnTipBinding | |||
| //拼写单词RecyclerView适配器 | |||
| private lateinit var spellAdapter : AdapterSpell | |||
| @@ -52,6 +69,11 @@ class LearnWordActivity : BaseActivityVM<ActivityLearnWordBinding, LearnWordView | |||
| private val SPELL_TAG_ALL_RIGHT = 1 //全对 | |||
| private val SPELL_TAG_IN_ERROR = 2 //可纠错 | |||
| //左侧按钮tag值 | |||
| private val LEFT_TAG_ANSWER = 1 //答案 | |||
| private val LEFT_TAG_CORRECT = 2 //正确 | |||
| private val LEFT_TAG_NEXT = 3 //下一条 | |||
| override fun initViewModel() : LearnWordViewModel { | |||
| return ViewModelProvider(this)[LearnWordViewModel::class.java] | |||
| } | |||
| @@ -62,6 +84,7 @@ class LearnWordActivity : BaseActivityVM<ActivityLearnWordBinding, LearnWordView | |||
| initHistoricalRoute() | |||
| initContentBinding() | |||
| initControlButton() | |||
| initBottom() | |||
| } | |||
| @@ -99,7 +122,9 @@ class LearnWordActivity : BaseActivityVM<ActivityLearnWordBinding, LearnWordView | |||
| private fun initHistoricalRoute() { | |||
| adapterHistorical = AdapterHistoricalRoute(vm.learnData.lesson.courseType).apply { | |||
| onItemClick = { v : View, position : Int, item : LearnWord -> | |||
| //历史轨迹的点击,改变当前选项 | |||
| vm.currentIsHistoricalItemClick = true | |||
| vm.currentLearnWord.value = item | |||
| } | |||
| } | |||
| binding.rvHistoricalRoute.apply { | |||
| @@ -110,11 +135,14 @@ class LearnWordActivity : BaseActivityVM<ActivityLearnWordBinding, LearnWordView | |||
| /** 布局内容初始 */ | |||
| private fun initContentBinding() { | |||
| incWorcDetailBinding = IncWordDetailBinding.inflate(layoutInflater, binding.detailLayout, true) | |||
| when (vm.learnData.lesson.courseType) { | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL -> { | |||
| bindingSpell = IncLearnSpellBinding.inflate(layoutInflater, binding.containerLayout, true) | |||
| incSpellTipBinding = IncSpellLearnTipBinding.inflate(layoutInflater, binding.detailLayout, true) | |||
| //拼写释义控件显示 | |||
| bindingSpell.tvExplain.visibility = View.VISIBLE | |||
| //初始拼写列表 | |||
| bindingSpell.spellRecyclerView.run { | |||
| layoutManager = GridLayoutManager(this@LearnWordActivity, 2, GridLayoutManager.HORIZONTAL, false) | |||
| addItemDecoration(SpellItemDecoration()) | |||
| @@ -122,54 +150,349 @@ class LearnWordActivity : BaseActivityVM<ActivityLearnWordBinding, LearnWordView | |||
| (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false | |||
| spellAdapter = AdapterSpell().apply { | |||
| onItemSpellingClickListener = itemSpellingClick | |||
| onItemRecoveryClick = itemSpellRecoveryClick | |||
| defaultSoundWay = vm.defaultSoundWay | |||
| } | |||
| adapter = spellAdapter | |||
| } | |||
| //拼写纠错按钮 | |||
| //拼写纠错按钮 点击事件 | |||
| bindingSpell.tvControlError.click { | |||
| when (it.tag as Int) { | |||
| SPELL_TAG_ALL_RIGHT -> vm.currentSpellIsCorrect = false | |||
| SPELL_TAG_ALL_RIGHT -> vm.currentSpellIsCorrect = false | |||
| SPELL_TAG_IN_ERROR -> { | |||
| vm.currentSpellIsCorrect = true | |||
| //下一条显示 | |||
| binding.incControlButton.tvLeft.visibility = View.VISIBLE | |||
| //取消纠错点击 | |||
| spellAdapter.isErrorRecovery = false | |||
| spellAdapter.closeErrorRecovery() | |||
| } | |||
| } | |||
| it.visibility = View.INVISIBLE | |||
| } | |||
| bindingSpell.incWord.tvWord.click { | |||
| //拼写完成点击才有效,拼写中点击无效 | |||
| if (!spellAdapter.isSpelling) | |||
| readWord() | |||
| } | |||
| } | |||
| AppConstants.COURSE_TYPE_ENGLISH_VOICE -> { | |||
| bindingWord = IncLearnWordBinding.inflate(layoutInflater, binding.containerLayout, true) | |||
| bindingWord.ivVoice.click { readWord() } | |||
| bindingWord.incWord.tvWord.click { readWord() } | |||
| } | |||
| else -> { | |||
| bindingWord = IncLearnWordBinding.inflate(layoutInflater, binding.containerLayout, true) | |||
| bindingWord.incWord.tvWord.visibility = View.VISIBLE | |||
| bindingWord.incWord.tvWord.click { readWord() } | |||
| } | |||
| } | |||
| } | |||
| //初始化按钮 | |||
| private fun initControlButton() { | |||
| when (vm.learnData.lesson.courseType) { | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL -> { | |||
| binding.incControlButton.run { | |||
| tvLeft.visibility = View.INVISIBLE | |||
| tvCenter.visibility = View.INVISIBLE | |||
| tvRight.visibility = View.INVISIBLE | |||
| //设置为下一条 | |||
| tvLeft.setText(R.string.control_next) | |||
| } | |||
| //其他课程默认按钮全部隐藏 | |||
| binding.incControlButton.run { | |||
| tvLeft.visibility = View.INVISIBLE | |||
| tvCenter.visibility = View.INVISIBLE | |||
| tvRight.visibility = View.INVISIBLE | |||
| } | |||
| //拼写,左侧一直为下一条 | |||
| if (vm.learnData.lesson.courseType == AppConstants.COURSE_TYPE_ENGLISH_SPELL) { | |||
| binding.incControlButton.tvLeft.setText(R.string.control_next) | |||
| binding.incControlButton.tvLeft.tag = LEFT_TAG_NEXT | |||
| } | |||
| //左侧按钮点击 | |||
| binding.incControlButton.tvLeft.click { | |||
| when (it.tag as Int) { | |||
| LEFT_TAG_ANSWER -> clickAnswer() | |||
| LEFT_TAG_CORRECT -> clickCorrect() | |||
| LEFT_TAG_NEXT -> clickNext() | |||
| } | |||
| } | |||
| //中间按钮点击 : 错误、不认识 拼写时,本按钮不显示 | |||
| binding.incControlButton.tvCenter.click { | |||
| clickError() | |||
| } | |||
| //右侧按钮点击: 重读 | |||
| binding.incControlButton.tvRight.click { | |||
| readWord() | |||
| } | |||
| } | |||
| //最底部 | |||
| private fun initBottom(){ | |||
| binding.tvPlay.visibility = View.GONE | |||
| binding.tvPlayStop.visibility = View.GONE | |||
| //有效时间 | |||
| vm.validTime.observe(this){ | |||
| binding.tvValidTime.text = "本次学习 ${DateUtil.formatGMT(it,DateUtil.FORMAT_2)}" | |||
| } | |||
| } | |||
| private val impListener = object : IMPListener { | |||
| override fun onMpState(state : EMediaState) { | |||
| when (state) { | |||
| EMediaState.RUNNING -> bindingWord.ivVoice.playAnimation() | |||
| EMediaState.COMPLETE, EMediaState.ERROR -> bindingWord.ivVoice.cancelAnimation() | |||
| } | |||
| //知识点学习的时候,需要将重读按钮隐藏 | |||
| AppConstants.COURSE_TYPE_CHINESE_COMPOSITION -> binding.incControlButton.tvRight.visibility = View.INVISIBLE | |||
| //其他课程默认按钮全部显示 | |||
| } | |||
| } | |||
| override fun loadData() { | |||
| binding.incDetail.initValue("","","") | |||
| vm.loadNext() | |||
| //发音数据 | |||
| AudioCache.audioLiveData.observe(this) { | |||
| it?.run { | |||
| if (vm.learnData.lesson.courseType == AppConstants.COURSE_TYPE_ENGLISH_VOICE && bindingWord.ivVoice.visibility == View.VISIBLE) MPManager.play( | |||
| it, listener = impListener) | |||
| else MPManager.play(it) | |||
| } ?: showToast("未找到发音文件") | |||
| } | |||
| //如果需要显示图片,则添加监听 | |||
| if (vm.isNeedLoadPhoto) { | |||
| //图片数据 | |||
| PhotoCache.photoLiveData.observe(this) { | |||
| it?.run { ImageLoader.loadImage(bindingWord.imgWord, it) } ?: let { bindingWord.imgWord.visibility = View.GONE } | |||
| } | |||
| } | |||
| //学习数据到来 | |||
| vm.currentLearnWord.observe(this) { | |||
| //历史轨迹记录添加 | |||
| if (!vm.currentIsHistoricalItemClick) { | |||
| adapterHistorical.addData(it) | |||
| binding.rvHistoricalRoute.scrollToPosition(adapterHistorical.itemCount - 1) | |||
| } | |||
| //界面初始 | |||
| when (vm.learnData.lesson.courseType) { | |||
| AppConstants.COURSE_TYPE_ENGLISH_VOICE -> initVoice(it) | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL -> initSpell(it) | |||
| else -> initWord(it) | |||
| } | |||
| } | |||
| //上传数据后的处理 | |||
| vm.saveDataLiveData.observe(this) { | |||
| when { | |||
| //学习完成的保存数据 | |||
| it -> showLearnOverDialog() | |||
| //学习未完成的保存数据 | |||
| else -> finish() | |||
| } | |||
| } | |||
| } | |||
| /**新数据到来,进行初始化*/ | |||
| @SuppressLint("SetTextI18n") | |||
| private fun initWord(learnWord : LearnWord) { | |||
| //发音 | |||
| readFirst() | |||
| //图片 | |||
| if (vm.isNeedLoadPhoto) { | |||
| bindingWord.imgWord.visibility = View.INVISIBLE | |||
| PhotoCache.get(vm.dbControlBase, learnWord.wordId) | |||
| } | |||
| //单词内容 | |||
| bindingWord.incWord.tvWord.apply { | |||
| //识字需单独处理 | |||
| text = if (vm.learnData.lesson.courseType == AppConstants.COURSE_TYPE_CHINESE_LITERACY) ViewUtil.literacyToHtmlWord( | |||
| learnWord.word, learnWord.showColor) else learnWord.word | |||
| setTextColor(ContextCompat.getColor(this@LearnWordActivity, learnWord.showColor)) //单词显示颜色 | |||
| } | |||
| //音标 | |||
| if (vm.learnData.lesson.subjectId == AppConstants.SUBJECT_ENGLISH) { | |||
| initFirstVisible(bindingWord.tvPhonetic, | |||
| ViewUtil.generatePhonetic(bindingWord.tvPhonetic, learnWord.phonetic_uk, learnWord.phonetic_us)) | |||
| } else { | |||
| initFirstVisible(bindingWord.tvPhonetic, learnWord.phonetic_cn) | |||
| } | |||
| //基本释义 | |||
| initFirstVisible(bindingWord.tvExplain, learnWord.basic_explanation) | |||
| //扩展释义 | |||
| initFirstVisible(bindingWord.tvExpandExplain, learnWord.extend_explanation) | |||
| if (bindingWord.tvExpandExplain.text.isNotEmpty()) { | |||
| bindingWord.tvExpandExplain.text = "扩展释义:${bindingWord.tvExpandExplain.text}" | |||
| } | |||
| //句型 | |||
| if (vm.learnData.lesson.lessonType == AppConstants.LESSON_TYPE_SENTENCE) { | |||
| initFirstVisible(bindingWord.tvPattern, learnWord.pattern) | |||
| } | |||
| //按钮 | |||
| binding.incControlButton.tvLeft.apply { | |||
| visibility = View.VISIBLE | |||
| setText(R.string.control_answer) | |||
| tag = LEFT_TAG_ANSWER | |||
| } | |||
| binding.incControlButton.tvCenter.visibility = View.INVISIBLE | |||
| binding.incControlButton.tvRight.visibility = when (vm.learnData.lesson.courseType) { | |||
| AppConstants.COURSEPACK_TYPE_CHINESE_LITERACY, AppConstants.COURSE_TYPE_CHINESE_PINYIN, AppConstants.COURSE_TYPE_ENGLISH_SOUNDMARK -> View.INVISIBLE | |||
| else -> View.VISIBLE | |||
| } | |||
| //详情 | |||
| incWorcDetailBinding.root.visibility = View.GONE | |||
| incWorcDetailBinding.initValue(learnWord.phrase, learnWord.example, learnWord.reference) | |||
| } | |||
| private fun initVoice(learnWord : LearnWord) { | |||
| initWord(learnWord) | |||
| bindingWord.ivVoice.visibility = View.VISIBLE | |||
| bindingWord.incWord.tvWord.visibility = View.INVISIBLE | |||
| } | |||
| private fun initSpell(learnWord : LearnWord) { | |||
| //释义 | |||
| bindingSpell.tvExplain.text = learnWord.basic_explanation | |||
| //拼写单词 | |||
| bindingSpell.incWord.tvWord.text = "" | |||
| //拼写单词初始拼写的颜色 | |||
| bindingSpell.incWord.tvWord.setTextColor(ContextCompat.getColor(this, R.color.main_text_color)) | |||
| //拼写内容适配器进行赋值 | |||
| spellAdapter.setData(vm.currentSpellOption) | |||
| //容错按钮不显示 | |||
| bindingSpell.tvControlError.visibility = View.INVISIBLE | |||
| //详情 详情不显示 | |||
| incWorcDetailBinding.root.visibility = View.GONE | |||
| incWorcDetailBinding.initValue(learnWord.phrase, learnWord.example, learnWord.reference) | |||
| //拼写提示显示 | |||
| incSpellTipBinding.root.visibility = View.VISIBLE | |||
| binding.incControlButton.tvLeft.visibility = View.INVISIBLE | |||
| binding.incControlButton.tvRight.visibility = View.INVISIBLE | |||
| } | |||
| /** 点击答案 */ | |||
| private fun clickAnswer() { | |||
| readWord() | |||
| //在新数据到来,进行初始的时候,对图片数据进行了判断,如果没有图片数据,则其为View.gone,否则为View.invisible | |||
| if (vm.isNeedLoadPhoto && bindingWord.imgWord.visibility == View.INVISIBLE) { | |||
| bindingWord.imgWord.visibility = View.VISIBLE | |||
| } | |||
| if (vm.learnData.lesson.courseType == AppConstants.COURSE_TYPE_ENGLISH_VOICE) { | |||
| bindingWord.ivVoice.visibility = View.GONE | |||
| bindingWord.incWord.tvWord.visibility = View.VISIBLE | |||
| adapterHistorical.showHistoricalRouteValue() | |||
| } | |||
| bindingWord.run { | |||
| showVisible(tvPhonetic) | |||
| showVisible(tvExplain) | |||
| showVisible(tvExpandExplain) | |||
| showVisible(tvPattern) | |||
| } | |||
| incWorcDetailBinding.root.visibility = View.VISIBLE | |||
| binding.incControlButton.tvLeft.apply { | |||
| setText(R.string.correct) | |||
| tag = LEFT_TAG_CORRECT | |||
| } | |||
| binding.incControlButton.tvCenter.visibility = View.VISIBLE | |||
| } | |||
| /**点击正确*/ | |||
| private fun clickCorrect() { | |||
| readWord() | |||
| //设tag为0 ,避免出现异常情况 | |||
| binding.incControlButton.tvLeft.tag = 0 | |||
| adapterHistorical.currentLearnOver() | |||
| vm.clickCorrect(adapterHistorical.currentIsLastSelect) | |||
| clickNext() | |||
| } | |||
| /**点击错误*/ | |||
| private fun clickError() { | |||
| readWord() | |||
| adapterHistorical.currentLearnOver() | |||
| binding.incControlButton.tvCenter.visibility = View.INVISIBLE | |||
| binding.incControlButton.tvLeft.run { | |||
| setText(R.string.control_next) | |||
| tag = LEFT_TAG_NEXT | |||
| } | |||
| if (vm.learnData.lesson.courseType == AppConstants.COURSE_TYPE_ENGLISH_VOICE) { | |||
| adapterHistorical.currentLearnOver() | |||
| } | |||
| vm.clickError(adapterHistorical.currentIsLastSelect) | |||
| } | |||
| /**点击下一条*/ | |||
| private fun clickNext() { | |||
| //拼写,需要处理历史轨迹 和在下一条的时候保存当前学习数据 | |||
| if (vm.learnData.lesson.courseType == AppConstants.COURSE_TYPE_ENGLISH_SPELL){ | |||
| when{ | |||
| //正确 | |||
| vm.currentSpellIsCorrect -> { | |||
| //传递正确 | |||
| vm.clickCorrect(adapterHistorical.currentIsLastSelect) | |||
| val value = vm.currentLearnWord.value!! | |||
| // 正常第一次不需要移除,第一个表达式则判断为第一次出现但是为学前测试错误 ,第二个表达式为小循环但不为小循环最后一次且后面还有数据则需要移除 | |||
| if ((value.first && value.repeatNum == 1) || (!value.first && value.repeatNum <vm.learnRuleUtil.insertInterval.size && vm.learnRuleUtil.isHaveNext)) | |||
| adapterHistorical.spellNeedRemoveCurrent() | |||
| } | |||
| else -> { | |||
| vm.clickError(adapterHistorical.currentIsLastSelect) | |||
| adapterHistorical.spellNeedRemoveCurrent() | |||
| } | |||
| } | |||
| } | |||
| //当前数据为历史轨迹点击 且 最后一条学习未完成 | |||
| if (vm.currentIsHistoricalItemClick && !adapterHistorical.lastIsLearnOver) { | |||
| skipToHistoricalLastItem() | |||
| } else { | |||
| vm.loadNext() | |||
| } | |||
| } | |||
| /** 滑动到最后一项进行点击 */ | |||
| private fun skipToHistoricalLastItem() { | |||
| val lastPosition = historicalLayoutManager.findLastCompletelyVisibleItemPosition() | |||
| val itemCount = historicalLayoutManager.itemCount | |||
| LogUtil.e("recyclerView smooth before -> completeVisibleItemPositon = $lastPosition , itemCount = $itemCount") | |||
| if (lastPosition == itemCount - 1) { | |||
| historicalLayoutManager.findViewByPosition(lastPosition)?.performClick() | |||
| } else { | |||
| binding.rvHistoricalRoute.addOnScrollListener(historicalScrollListener) | |||
| binding.rvHistoricalRoute.smoothScrollToPosition(adapterHistorical.itemCount - 1) | |||
| } | |||
| //other way | |||
| // historicalLayoutManager.scrollToPositionWithOffset(adapterHistorical.itemCount - 1,0) | |||
| // historicalLayoutManager.findViewByPosition(historicalLayoutManager.findLastCompletelyVisibleItemPosition())?.performClick() | |||
| } | |||
| private val historicalScrollListener = object : OnScrollListener() { | |||
| override fun onScrollStateChanged(recyclerView : RecyclerView, newState : Int) { | |||
| super.onScrollStateChanged(recyclerView, newState) | |||
| if (newState == RecyclerView.SCROLL_STATE_IDLE) { | |||
| binding.rvHistoricalRoute.removeOnScrollListener(this) | |||
| historicalLayoutManager.findViewByPosition(adapterHistorical.itemCount - 1)?.performClick() | |||
| } | |||
| } | |||
| } | |||
| /** 新单词来的初始设置 : 没有值则GONE,有值则INVISIBLE */ | |||
| private fun initFirstVisible(view : TextView, value : String?) { | |||
| view.text = value | |||
| if (value.isNullOrEmpty()) { | |||
| view.visibility = View.GONE | |||
| } else { | |||
| view.visibility = View.INVISIBLE | |||
| } | |||
| } | |||
| /* TextView设置可见: 前提条件是textview有值且不为空,否则设置为不可见 */ | |||
| private fun showVisible(view : TextView) { | |||
| if (view.text.isNullOrEmpty()) { | |||
| view.visibility = View.GONE | |||
| } else { | |||
| view.visibility = View.VISIBLE | |||
| } | |||
| } | |||
| /** | |||
| * 拼写时的点击事件 | |||
| @@ -179,32 +502,163 @@ class LearnWordActivity : BaseActivityVM<ActivityLearnWordBinding, LearnWordView | |||
| * @param correctValue 正确的拼写值,但未选中的赋值了颜色 | |||
| * @param errorSize 错误的个数,拼写完成后才会有这个值 | |||
| */ | |||
| private val itemSpellingClick = | |||
| { selectedValue : SpannableStringBuilder, nextPosition : Int, isOver : Boolean, correctValue : SpannableStringBuilder, errorSize : Int -> | |||
| when { | |||
| isOver -> { //拼写结束 | |||
| //设置正确的值 | |||
| bindingSpell.incWord.tvWord.setTextColor(ContextCompat.getColor(this, R.color.red_1)) | |||
| bindingSpell.incWord.tvWord.text = correctValue | |||
| //拼写结束 | |||
| // vm.spellOver(selectedValue.toString(), errorSize) | |||
| //执行当前的单词发音 延迟发音 | |||
| // bindingSpell.incWord.tvWord.postDelayed({ ivVoiceClick(spellBinding.incWord.tvWord) }, 200) | |||
| } | |||
| else -> { | |||
| //设置为选中的值 | |||
| bindingSpell.incWord.tvWord.text = selectedValue | |||
| private val itemSpellingClick = { selectedValue : SpannableStringBuilder, nextPosition : Int, isOver : Boolean, correctValue : SpannableStringBuilder, errorSize : Int -> | |||
| when { | |||
| isOver -> { //拼写结束 | |||
| //执行当前的单词发音 延迟发音 | |||
| bindingSpell.incWord.tvWord.postDelayed({ readWord() }, 200) | |||
| //设置正确的值 | |||
| bindingSpell.incWord.tvWord.setTextColor(ContextCompat.getColor(this, vm.currentLearnWord.value!!.showColor)) | |||
| bindingSpell.incWord.tvWord.text = correctValue | |||
| //拼写是否正确 | |||
| vm.currentSpellIsCorrect = selectedValue == correctValue | |||
| //纠错按钮 | |||
| when { | |||
| vm.currentSpellIsCorrect -> { | |||
| bindingSpell.tvControlError.apply { | |||
| visibility = View.VISIBLE | |||
| text = "操作失误,我不会" | |||
| tag = SPELL_TAG_ALL_RIGHT | |||
| } | |||
| binding.incControlButton.tvLeft.visibility = View.VISIBLE | |||
| } | |||
| (correctValue.length >= 7 && errorSize <= 3) || (correctValue.length >= 5 && errorSize <= 2) || (correctValue.length >= 4 && errorSize == 1) -> { | |||
| bindingSpell.tvControlError.apply { | |||
| visibility = View.VISIBLE | |||
| text = "操作失误,我会" | |||
| tag = SPELL_TAG_IN_ERROR | |||
| } | |||
| //进入纠错状态 | |||
| spellAdapter.openErrorRecovery() | |||
| } | |||
| //错误,不显示容错按钮 进入纠错状态 | |||
| else -> spellAdapter.openErrorRecovery() | |||
| } | |||
| //拼写提示隐藏 | |||
| incSpellTipBinding.root.visibility = View.GONE | |||
| //显示详情 | |||
| incWorcDetailBinding.root.visibility = View.VISIBLE | |||
| //显示重读按钮 | |||
| binding.incControlButton.tvRight.visibility = View.VISIBLE | |||
| } | |||
| bindingSpell.spellRecyclerView.scrollToPosition(nextPosition) | |||
| // 拼写未完成 设置为选中的值 | |||
| else -> bindingSpell.incWord.tvWord.text = selectedValue | |||
| } | |||
| bindingSpell.spellRecyclerView.scrollToPosition(nextPosition) | |||
| } | |||
| /** | |||
| * 纠错时点击事件 | |||
| * @param showValue 用于显示的值 | |||
| * @param isOver 纠错是否完成 | |||
| * @param nextPositon 纠错中的下一项 | |||
| */ | |||
| private val itemSpellRecoveryClick = { showValue : SpannableStringBuilder, isOver : Boolean, nextPosition : Int -> | |||
| bindingSpell.incWord.tvWord.text = showValue | |||
| //纠错完成 | |||
| if (isOver){ | |||
| binding.incControlButton.tvLeft.visibility = View.VISIBLE | |||
| } | |||
| bindingSpell.spellRecyclerView.scrollToPosition(nextPosition) | |||
| } | |||
| /** 英语音标、语文拼音、语文识字在词条出现时,先播放di.mp3 */ | |||
| private fun readFirst() { | |||
| when (vm.learnData.lesson.courseType) { | |||
| AppConstants.COURSE_TYPE_CHINESE_PINYIN, AppConstants.COURSE_TYPE_CHINESE_LITERACY, AppConstants.COURSE_TYPE_ENGLISH_SOUNDMARK, AppConstants.COURSE_TYPE_CHINESE_COMPOSITION -> MPManager.playAsset( | |||
| "common_voice/di.mp3") | |||
| //拼写初始不发音 | |||
| AppConstants.COURSE_TYPE_ENGLISH_SPELL -> { | |||
| } | |||
| //单词、辨音发音 | |||
| else -> readWord() | |||
| } | |||
| } | |||
| private fun readWord() { | |||
| when (vm.learnData.lesson.coursePackType) { | |||
| AppConstants.COURSEPACK_TYPE_ENGLISH_WORD, AppConstants.COURSEPACK_TYPE_ENGLISH_SOUNDMARK, AppConstants.COURSEPACK_TYPE_ENGLISH_SPOKEN, AppConstants.COURSEPACK_TYPE_CHINESE_LITERACY, AppConstants.COURSEPACK_TYPE_CHINESE_PINYIN -> { | |||
| val valueWord = vm.currentLearnWord.value!! | |||
| AudioCache.get(vm.dbControlBase, valueWord.wordId, vm.defaultSoundWay) | |||
| } | |||
| } | |||
| } | |||
| override fun dispatchTouchEvent(ev : MotionEvent?) : Boolean { | |||
| //执行有效计时 | |||
| vm.executeLearnValidTime() | |||
| return super.dispatchTouchEvent(ev) | |||
| } | |||
| /** 返回 */ | |||
| override fun onBackPressed() { | |||
| when { | |||
| vm.isAllOver -> finish() | |||
| else -> "" | |||
| else -> when { | |||
| vm.isHasLearned -> { | |||
| CommonDialog.newInstance( | |||
| CommonDialogBean(titleText = R.string.quit_learn_title, contentText = R.string.quit_learn_content, | |||
| leftText = R.string.quit, rightText = R.string.cancel)).apply { | |||
| onCommonDialogButtonClickListener = { dialog, isRightClick -> | |||
| dialog.dismissAllowingStateLoss() | |||
| when { | |||
| // TODO: 2022/4/26 取消,恢复计时 | |||
| isRightClick -> "" | |||
| // TODO: 2022/4/26 保存数据 | |||
| else -> vm.saveData() | |||
| } | |||
| } | |||
| }.show(supportFragmentManager, "learn_back_dialog") | |||
| } | |||
| else -> finish() | |||
| } | |||
| } | |||
| } | |||
| /** 学习完成 */ | |||
| private fun showLearnOverDialog() { | |||
| vm.loadAfterTest().observe(this) { showTimeCount -> | |||
| LearnDialog.newInstance(LearnDialogBean(AppConstants.DIALOG_TYPE_LEARN_OVER).apply { | |||
| correctNumber = vm.learnData.lesson.correctNumber + vm.learnRuleUtil.currentCorrectMap.size | |||
| errorNumber = vm.learnData.lesson.errorNumber + vm.learnRuleUtil.currentErrorMap.size | |||
| this.showTimeCount = showTimeCount | |||
| }).apply { | |||
| onDialogListener = { action, dialog -> | |||
| when (action) { | |||
| //重学动作 | |||
| AppConstants.DIALOG_LESSON_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.learnData.lesson.subjectId, | |||
| vm.learnData.lesson.courseId, | |||
| AppConstants.ACTION_LESSON_AFTER_TEST_RELEARN).apply { | |||
| leesonPositionIndex = vm.learnData.lesson.lessonPositionInList | |||
| } | |||
| } | |||
| } | |||
| }.show(childFragmentManager,"lesson_relearn_tip") | |||
| // 开始学后测试 | |||
| AppConstants.DIALOG_START_TEST -> { | |||
| dialog.dismissAllowingStateLoss() | |||
| //发送动作 : 学后测试 | |||
| LiveDataBus.with<LearnEventData>(AppConstants.EVENT_COURSE).value = | |||
| LearnEventData(vm.learnData.lesson.subjectId, | |||
| vm.learnData.lesson.courseId, | |||
| AppConstants.ACTION_LESSON_AFTER_TEST_AGAIN).apply { | |||
| leesonPositionIndex = vm.learnData.lesson.lessonPositionInList | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }.show(supportFragmentManager, "learn_over_dialog") | |||
| } | |||
| } | |||
| @@ -1,22 +1,305 @@ | |||
| package com.xkl.cdl.module.learn | |||
| 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 | |||
| import com.suliang.common.util.DateUtil | |||
| import com.suliang.common.util.LogUtil | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.DataTransferHolder | |||
| import com.xkl.cdl.data.bean.LearnWord | |||
| import com.xkl.cdl.data.bean.SpellItemBean | |||
| import com.xkl.cdl.data.bean.intentdata.LearnData | |||
| import com.xkl.cdl.data.event.LearnEventData | |||
| import com.xkl.cdl.data.manager.CourseManager | |||
| 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.util.LearnRuleUtil | |||
| import io.reactivex.rxjava3.core.Observable | |||
| import kotlinx.coroutines.delay | |||
| import kotlinx.coroutines.launch | |||
| import mqComsumerV1.Struct | |||
| import mqComsumerV1.Struct.LearnDuration | |||
| import mqComsumerV1.Struct.LearnEntity | |||
| /** | |||
| * 正常的学习、复习、自动播放 | |||
| */ | |||
| class LearnWordViewModel : LearnBaseViewModel() { | |||
| //记录是否是历史轨迹的item点击 | |||
| var currentIsHistoricalItemClick : Boolean = false | |||
| //默认发音 | |||
| var defaultSoundWay : Int = 0 | |||
| //操作数据 | |||
| val learnData = DataTransferHolder.instance.getData<LearnData>().apply { | |||
| DataTransferHolder.instance.clear() | |||
| } | |||
| val dbControlBase = DbControlBase(learnData.lesson.subjectId, learnData.lesson.coursePackId, learnData.lesson.coursePackType, | |||
| learnData.lesson.courseId, learnData.lesson.courseType) | |||
| //是否需要显示单词图片 | |||
| var isNeedLoadPhoto = when (learnData.lesson.coursePackType) { | |||
| AppConstants.COURSEPACK_TYPE_ENGLISH_WORD, AppConstants.COURSEPACK_TYPE_ENGLISH_SOUNDMARK, AppConstants.COURSEPACK_TYPE_CHINESE_LITERACY, AppConstants.COURSEPACK_TYPE_CHINESE_PINYIN -> true | |||
| else -> false | |||
| } | |||
| //当前学习的word | |||
| val currentLearnWord = MutableLiveData<LearnWord>() | |||
| //记录拼写的最终结果 | |||
| //当前拼写内容的选项 | |||
| //当前拼写选项 | |||
| var currentSpellOption = mutableListOf<SpellItemBean>() | |||
| //记录拼写的最终结果 是否正确 | |||
| var currentSpellIsCorrect = false | |||
| //规则数据初始 | |||
| val learnRuleUtil = LearnRuleUtil<LearnWord>(learnData.learnWordList, | |||
| learnData.lesson.courseType == AppConstants.COURSE_TYPE_ENGLISH_SPELL, | |||
| learnData.examErrorMap) | |||
| //是否有学习的数据,避免进入没有学习,直接退出时,进行数据保存 | |||
| var isHasLearned = false | |||
| //记录当前课时的学习进度位置 | |||
| var currentLessonLearnedPosition = learnData.lesson.learnedIndex | |||
| //数据上传完成监听: true 学习完成的上传 false可能是半途的返回事件,直接退出 | |||
| val saveDataLiveData = MutableLiveData<Boolean>() | |||
| /** 获取数据 */ | |||
| fun loadNext() { | |||
| //修改标记 | |||
| currentIsHistoricalItemClick = false | |||
| val word = learnRuleUtil.loadNext | |||
| word?.let { | |||
| //拼写时,需要对当前内容选项进行拼接 | |||
| if (learnData.lesson.courseType == AppConstants.COURSE_TYPE_ENGLISH_SPELL) { | |||
| initSpellOption(it.word) | |||
| } | |||
| currentLearnWord.value = it | |||
| } ?: let { //没有数据 即学习完成 | |||
| isAllOver = true | |||
| //停止总计时 | |||
| stopTotalCountTing() | |||
| // TODO: 2022/4/26 停止有效计时 | |||
| //封装保存数据 | |||
| saveData() | |||
| } | |||
| } | |||
| /** 初始化拼写选项集合 */ | |||
| private fun initSpellOption(word : String) { | |||
| currentSpellOption.clear() | |||
| word.forEachIndexed { index, c -> | |||
| val newChar = c.createRandomNewChar() | |||
| if ((0 .. 1).random() == 0) { | |||
| currentSpellOption.add(SpellItemBean(c, true)) | |||
| currentSpellOption.add(SpellItemBean(newChar, false)) | |||
| } else { | |||
| currentSpellOption.add(SpellItemBean(newChar, false)) | |||
| currentSpellOption.add(SpellItemBean(c, true)) | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * 点击正确 | |||
| */ | |||
| fun clickCorrect(currentIsLastSelect : Boolean) { | |||
| isHasLearned = true | |||
| currentLearnWord.value?.let { | |||
| saveCurrentLearnData(it, currentIsLastSelect, true) | |||
| learnRuleUtil.correct(it, currentIsLastSelect) | |||
| //设置已学位置 | |||
| setLearnPoint(currentIsLastSelect, it.first) | |||
| } | |||
| } | |||
| /** | |||
| * 点击错误 | |||
| */ | |||
| fun clickError(currentIsLastSelect : Boolean) { | |||
| isHasLearned = true | |||
| currentLearnWord.value?.let { | |||
| saveCurrentLearnData(it, currentIsLastSelect, false) | |||
| learnRuleUtil.error(it) | |||
| setLearnPoint(currentIsLastSelect, it.first) | |||
| } | |||
| } | |||
| /** 设置已学位置 */ | |||
| private fun setLearnPoint(currentIsLastSelect : Boolean, isCycleFirst : Boolean) { | |||
| if (currentIsLastSelect && isCycleFirst) { | |||
| currentLessonLearnedPosition++ | |||
| } | |||
| } | |||
| //保存学习记录 | |||
| private val record = Struct.Record.newBuilder() | |||
| /** | |||
| * 保存当前学习数据 | |||
| * @param item LearnWord | |||
| * @param isLastItem Boolean | |||
| * @param isCorrect Boolean | |||
| */ | |||
| private fun saveCurrentLearnData(item : LearnWord, isLastItem : Boolean, isCorrect : Boolean) { | |||
| val key = "${item.chapterId}_${item.lessonId}_${item.wordId}" | |||
| when { | |||
| //正确 | |||
| isCorrect -> when { | |||
| //第一次正确 | |||
| isLastItem && item.first -> record.addEntity(createBaseLearnEntity(item.wordId).apply { | |||
| if (learnRuleUtil.isInExamErrorMap(key)) { | |||
| isOnlySavePoint = true //设置仅保存学习点 | |||
| } | |||
| }) | |||
| } | |||
| //错误 | |||
| else -> { | |||
| val newEntityBuilder = createBaseLearnEntity(item.wordId) | |||
| when { | |||
| //第一次错误 | |||
| isLastItem && item.first -> when { | |||
| //在错误列表中 | |||
| learnRuleUtil.isInExamErrorMap(key) -> newEntityBuilder.setIsOnlySavePoint(true) | |||
| .setIsError(true) | |||
| .setReviewNum(1).lastReviewDate = getCurrentDateWithString() | |||
| else -> newEntityBuilder.isError = true | |||
| } | |||
| //历史轨迹错误 | |||
| else -> newEntityBuilder.apply { | |||
| isFromTrack = true | |||
| //上一次为错误还是正确:上一次结果在RuleUtil的正确或错误集合中,不在错误就在正确 | |||
| lastIsError = learnRuleUtil.isInCurrentErrorMap(key) | |||
| isError = true | |||
| reviewNum = 1 | |||
| lastReviewDate = getCurrentDateWithString() | |||
| } | |||
| } | |||
| record.addEntity(newEntityBuilder) | |||
| } | |||
| } | |||
| } | |||
| /** 生成一个基本的LearnEntity */ | |||
| private fun createBaseLearnEntity(wordId : Long) : LearnEntity.Builder { | |||
| return Struct.LearnEntity.newBuilder().apply { | |||
| learnData.lesson.let { | |||
| projectId = it.subjectId.toLong() | |||
| packId = it.coursePackId | |||
| courseId = it.courseId | |||
| chapterId = it.chapterId | |||
| lessonId = it.lessonId | |||
| entityId = wordId | |||
| } | |||
| tag = "android" | |||
| created = getCurrentDateWithString() | |||
| } | |||
| } | |||
| private fun getCurrentDateWithString() = DateUtil.format(System.currentTimeMillis(), DateUtil.FORMAT_1) | |||
| /** | |||
| * 保存当前的单词学习时长 | |||
| */ | |||
| fun saveCurrentLearnDuration() : LearnDuration.Builder? { | |||
| return LearnDuration.newBuilder() | |||
| .setProjectId(learnData.lesson.subjectId.toLong()) | |||
| .setPackId(learnData.lesson.coursePackId) | |||
| .setCategoryId(learnData.lesson.coursePackId) | |||
| .setCourseId(learnData.lesson.courseId) | |||
| .setCreated(getCurrentDateWithString()) | |||
| .setTag("android") | |||
| .setTimeFrame(DateUtil.getTimeFrame(System.currentTimeMillis())) //设置时段 | |||
| .setIsReview(false) //是否复习时长 | |||
| .setDuration(validTime.value!!) //时长,毫秒 | |||
| .setTotalDuration(totalUseTime.value!!) //总时间,包含空闲部分 | |||
| } | |||
| private var saveInit = false | |||
| /** 封装保存数据,保存完后后,通过currentLearnWord为空通知activity显示完成界面*/ | |||
| fun saveData() { | |||
| showHideLoading(true) | |||
| Observable.create<Boolean> { | |||
| viewModelScope.launch { | |||
| delay(1000) | |||
| it.onNext(true) | |||
| } | |||
| // TODO: 2022/4/14 传递保存record信息 | |||
| // record 已经实例化并已经将数据保存 | |||
| if (!saveInit) { | |||
| learnData.lesson.apply { | |||
| learnedIndex = currentLessonLearnedPosition | |||
| correctNumber += learnRuleUtil.currentCorrectMap.size | |||
| errorNumber += learnRuleUtil.currentErrorMap.size | |||
| learnIsOver = (correctNumber + errorNumber == totalNumber) | |||
| } | |||
| //添加到错误本集合中,主要用于小游戏练习(学前总 课时都没有添加,从这里添加到集合后发送出去,添加到集合) | |||
| learnData.examErrorMap?.putAll(learnRuleUtil.currentErrorMap) | |||
| record.addDuration(saveCurrentLearnDuration()) | |||
| saveInit = true | |||
| } | |||
| DataRepository.saveRecord(record) | |||
| // LogUtil.e(JsonFormat.printToString(record.build())) | |||
| }.compose(diskIo2Main()).subscribe({ | |||
| showHideLoading(false) | |||
| sendEventBus() //返回发送数据 | |||
| //数据保存完成后,通过数据为空进行通知,测试完成 | |||
| saveDataLiveData.value = isAllOver | |||
| }, { | |||
| showHideLoading(false) | |||
| it.printStackTrace() | |||
| }) | |||
| } | |||
| /** 查询生成 课时后测试的数量和题数 */ | |||
| fun loadAfterTest() : MutableLiveData<String> { | |||
| val result = MutableLiveData<String>() | |||
| Observable.fromCallable { | |||
| val queryLearnTest = DBCourseManager.queryLearnTest(dbControlBase, AppConstants.TEST_TYPE_AFTER, learnData.lesson) | |||
| CourseManager.expectedTestTime(learnData.lesson.courseType, AppConstants.TEST_TYPE_AFTER, queryLearnTest) | |||
| }.compose(diskIo2Main()).subscribe { | |||
| result.value = it | |||
| } | |||
| return result | |||
| } | |||
| /** 发送数据事件 */ | |||
| private fun sendEventBus() { | |||
| LiveDataBus.withNonSticky<LearnEventData>(AppConstants.EVENT_COURSE).value = LearnEventData(learnData.lesson.subjectId, | |||
| learnData.lesson.courseId, | |||
| AppConstants.DATA_LESSON_LEARN_OVER).apply { | |||
| this.leesonPositionIndex = learnData.lesson.lessonPositionInList | |||
| this.newErrorMap = learnData.examErrorMap | |||
| } | |||
| } | |||
| override fun onResume(owner : LifecycleOwner) { | |||
| super.onResume(owner) | |||
| if (!isAllOver) startTotalCounting() | |||
| } | |||
| override fun onPause(owner : LifecycleOwner) { | |||
| super.onPause(owner) | |||
| stopTotalCountTing() | |||
| } | |||
| } | |||
| @@ -1,10 +1,14 @@ | |||
| package com.xkl.cdl.module.m_center_learn.coursechildren | |||
| import android.view.View | |||
| import androidx.constraintlayout.widget.ConstraintLayout | |||
| import androidx.lifecycle.ViewModelProvider | |||
| import androidx.recyclerview.widget.LinearLayoutManager | |||
| import androidx.recyclerview.widget.RecyclerView | |||
| import com.suliang.common.base.fragment.BaseFragmentVM | |||
| import com.suliang.common.eventbus.LiveDataBus | |||
| import com.suliang.common.util.DateUtil | |||
| import com.xkl.cdl.R | |||
| import com.xkl.cdl.adapter.AdapterLesson | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.DataTransferHolder | |||
| @@ -12,34 +16,37 @@ 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.bean.intentdata.LearnData | |||
| import com.xkl.cdl.data.event.LearnEventData | |||
| import com.xkl.cdl.data.manager.CourseManager | |||
| import com.xkl.cdl.data.repository.DataRepository | |||
| import com.xkl.cdl.databinding.FragmentCourseLessonBinding | |||
| import com.xkl.cdl.dialog.LearnDialog | |||
| import com.xkl.cdl.module.learn.LearnWordActivity | |||
| import mqComsumerV1.Struct | |||
| import java.text.DateFormat | |||
| /** | |||
| * 课程章节目录 | |||
| */ | |||
| class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseMainFragmentViewModel>() { | |||
| companion object { | |||
| @JvmStatic | |||
| fun newInstance() = CourseLessonFragment() | |||
| } | |||
| override fun initViewModel(): CourseMainFragmentViewModel { | |||
| override fun initViewModel() : CourseMainFragmentViewModel { | |||
| return ViewModelProvider(requireParentFragment())[CourseMainFragmentViewModel::class.java] | |||
| } | |||
| private lateinit var adapterLesson: AdapterLesson | |||
| private lateinit var adapterLesson : AdapterLesson | |||
| override fun initFragment() { | |||
| //口语显示顶部的自动播放与跟读模式 | |||
| if (vm.course.courseType == AppConstants.COURSE_TYPE_ENGLISH_SPOKEN){ | |||
| if (vm.course.courseType == AppConstants.COURSE_TYPE_ENGLISH_SPOKEN) { | |||
| binding.spokenTopLayout.visibility = View.VISIBLE | |||
| } | |||
| binding.recyclerView.apply { | |||
| layoutManager = LinearLayoutManager(requireContext(),LinearLayoutManager.VERTICAL,false) | |||
| layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) | |||
| adapterLesson = AdapterLesson(vm).apply { | |||
| onItemClick = onLessonClick | |||
| } | |||
| @@ -48,15 +55,15 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| //设置数据 | |||
| (binding.recyclerView.adapter as AdapterLesson).setData(vm.allLesson.toMutableList()) | |||
| } | |||
| override fun loadData() { | |||
| //监听数据传递事件 | |||
| listenerLiveBus() | |||
| } | |||
| private fun 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 | |||
| @@ -68,8 +75,8 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| //获取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) | |||
| 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) | |||
| } | |||
| @@ -77,23 +84,57 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| } | |||
| //课时学前测试结束,开始学习 -> 进入学习界面 | |||
| AppConstants.ACTION_LESSON_BEFORE_TEST_OVER_START_LEARN -> { | |||
| startLearn(vm.allLesson[learnEventData.leesonPositionIndex]) | |||
| } | |||
| //课时学习结束 | |||
| AppConstants.DATA_LESSON_LEARN_OVER -> { | |||
| // 更新Lesson 更新detail错误条目数 | |||
| adapterLesson.notifyItemChanged(learnEventData.leesonPositionIndex) | |||
| vm.allLesson[learnEventData.leesonPositionIndex].let { | |||
| val key = "${it.chapterId}_${it.lessonId}" | |||
| vm.courseDetail.wrong.put(key, it.errorNumber) | |||
| //添加到错误本中,现在主要用于小游戏取值 | |||
| learnEventData.newErrorMap?.forEach { | |||
| vm.courseDetail.temporary_words.put(it.key, "") | |||
| } | |||
| //更新课时学习点 | |||
| vm.courseDetail.lesson_learn_point.put("${it.chapterId}_${it.lessonId}", it.wordIds[it.learnedIndex]) | |||
| //更新课程学习点 | |||
| vm.courseDetail.course_learn_point = "${it.chapterId}_${it.lessonId}_${it.wordIds[it.learnedIndex]}" | |||
| } | |||
| //课程进度 | |||
| val courseProgress = when (vm.course.coursePackType) { | |||
| AppConstants.COURSEPACK_TYPE_CHINESE_COMPOSITION -> CourseManager.calculateCompositionCourseProgess( | |||
| vm.allLesson) | |||
| AppConstants.COURSEPACK_TYPE_ENGLISH_SPOKEN -> CourseManager.calculateSpokenCourseProgress(vm.allLesson) | |||
| else -> CourseManager.calculateEnglishCourseProgress(vm.allLesson) | |||
| } | |||
| vm.courseDetail.courseLearnProgress = courseProgress | |||
| vm.course.courseLearnProgress = courseProgress | |||
| //项目总进度 | |||
| val subjectProgress = CourseManager.calculateSubjectProgress(vm.course.subjectId, vm.course.coursePackId, | |||
| vm.course.courseId, courseProgress) | |||
| //保存 | |||
| vm.updateLearnSchedule(courseProgress, subjectProgress) | |||
| } | |||
| //学后测试结束传递数据回来更新数据 | |||
| 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) | |||
| 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]) | |||
| @@ -101,63 +142,109 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| AppConstants.ACTION_LESSON_AFTER_TEST_NEXT -> { | |||
| //下一课时 or 下一步(提示课程学习完成) | |||
| // TODO: 2022/4/22 判断课程是否学习完成,学习完成,显示课程学习完成弹窗,否则进入下一个没有学习完成的课时,进行学习 | |||
| when(vm.course.courseLearnProgress){ | |||
| 100.0 -> showCourseOverDialog() | |||
| else -> { | |||
| var nextLessonPosition = -1 | |||
| //从下一课时开始到最后一个课时 | |||
| for ( i in learnEventData.leesonPositionIndex + 1 until vm.allLesson.size){ | |||
| if (!vm.allLesson[i].learnIsOver) { | |||
| nextLessonPosition = i | |||
| break | |||
| } | |||
| } | |||
| if (nextLessonPosition == -1) { | |||
| //从0向后循环 | |||
| for (i in 0 until learnEventData.leesonPositionIndex) { | |||
| if (!vm.allLesson[i].learnIsOver) { | |||
| nextLessonPosition = i | |||
| break | |||
| } | |||
| } | |||
| } | |||
| // 滑动到指定课时,进行动态设置点击 | |||
| val linearLayoutManager = binding.recyclerView.layoutManager as LinearLayoutManager | |||
| val findFirstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition() //第一个可见位置 | |||
| val findLastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition() //最后一个可见位置 | |||
| if (nextLessonPosition in findFirstVisibleItemPosition .. findLastVisibleItemPosition){ | |||
| linearLayoutManager.findViewByPosition(nextLessonPosition)?.findViewById<ConstraintLayout>(R.id.layout_content)?.performClick() | |||
| }else{ | |||
| recycleViewScrollListener.lessonPosition = nextLessonPosition | |||
| binding.recyclerView.addOnScrollListener(recycleViewScrollListener) | |||
| binding.recyclerView.smoothScrollToPosition(nextLessonPosition) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** 课时滑动监听 */ | |||
| private val recycleViewScrollListener = object : RecyclerView.OnScrollListener() { | |||
| //需要滑动到的指定的位置 | |||
| var lessonPosition = 0 | |||
| override fun onScrollStateChanged(recyclerView : RecyclerView, newState : Int) { | |||
| super.onScrollStateChanged(recyclerView, newState) | |||
| if (newState == RecyclerView.SCROLL_STATE_IDLE){ | |||
| binding.recyclerView.removeOnScrollListener(this) | |||
| val linearLayoutManager = binding.recyclerView.layoutManager as LinearLayoutManager | |||
| linearLayoutManager.findViewByPosition(lessonPosition)?.findViewById<ConstraintLayout>(R.id.layout_content)?.performClick() | |||
| } | |||
| } | |||
| } | |||
| /** 课时点击实现 */ | |||
| private val onLessonClick : (v:View,position: Int, lesson: Lesson) -> Unit = { view, position, entity -> | |||
| when(entity.lessonType){ | |||
| private val onLessonClick : (v : View, position : Int, lesson : Lesson) -> Unit = { view, position, entity -> | |||
| when (entity.lessonType) { | |||
| AppConstants.LESSON_TYPE_WORD -> { | |||
| startLearn(entity) | |||
| /* if (entity.beforeTestScore == AppConstants.NOT_DOING){ //课时学前测试,没有做 | |||
| //弹窗显示学前测试提示 | |||
| showLessonBeforeTestStartDialog(entity) | |||
| }else if (!entity.learnIsOver){ //当前课时未学完,直接开始学习 | |||
| startLearn(entity) | |||
| }else if (entity.afterTestScore != AppConstants.NOT_DOING){ | |||
| loadLessonAfterTest(entity) | |||
| }else{ //当前课时学习完成的弹窗 | |||
| showLessonAllOverDialog(entity) | |||
| }*/ | |||
| /* if (entity.beforeTestScore == AppConstants.NOT_DOING){ //课时学前测试,没有做 | |||
| //弹窗显示学前测试提示 | |||
| showLessonBeforeTestStartDialog(entity) | |||
| }else if (!entity.learnIsOver){ //当前课时未学完,直接开始学习 | |||
| startLearn(entity) | |||
| }else if (entity.afterTestScore != AppConstants.NOT_DOING){ | |||
| loadLessonAfterTest(entity) | |||
| }else{ //当前课时学习完成的弹窗 | |||
| showLessonAllOverDialog(entity) | |||
| }*/ | |||
| } | |||
| AppConstants.LESSON_TYPE_SENTENCE -> { | |||
| } | |||
| AppConstants.LESSON_TYPE_DIALOGUE -> { | |||
| } | |||
| AppConstants.LESSON_TYPE_COMPOSITION_VIDEO -> { | |||
| } | |||
| AppConstants.LESSON_TYPE_COMPOSITION_KNOWLEDGE -> { | |||
| } | |||
| AppConstants.LESSON_TYPE_COMPOSITION_EXAM -> { | |||
| } | |||
| AppConstants.LESSON_TYPE_COMPOSITION_READING -> { | |||
| } | |||
| AppConstants.LESSON_TYPE_COMPOSITION_TASK -> { | |||
| } | |||
| } | |||
| } | |||
| /** 显示课时前测试开始的弹窗 | |||
| * 先获取数据,然后显示弹窗 | |||
| * */ | |||
| private fun showLessonBeforeTestStartDialog(lesson:Lesson){ | |||
| vm.loadTest(AppConstants.TEST_TYPE_BEFORE,lesson).observe(this){ | |||
| 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 | |||
| @@ -172,7 +259,7 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| // TODO: 2022/4/21 进入学习界面 | |||
| } | |||
| // 开始测试 | |||
| AppConstants.DIALOG_START_TEST -> startLessonTest(lesson,AppConstants.TEST_TYPE_BEFORE,it) | |||
| AppConstants.DIALOG_START_TEST -> startLessonTest(lesson, AppConstants.TEST_TYPE_BEFORE, it) | |||
| } | |||
| } | |||
| }.show(childFragmentManager, "lesson_before_test_start") | |||
| @@ -183,8 +270,8 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| * 查询课时数据,并开始学习,进入学习界面 | |||
| * @param lesson Lesson 课时 | |||
| */ | |||
| private fun startLearn(lesson : Lesson){ | |||
| vm.loadLessonLearnData(lesson).observe(this){ | |||
| private fun startLearn(lesson : Lesson) { | |||
| vm.loadLessonLearnData(lesson).observe(this) { | |||
| DataTransferHolder.instance.putData(value = it) | |||
| startActivity(LearnWordActivity::class.java) | |||
| } | |||
| @@ -194,17 +281,16 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| /** 开始课时学后测试 | |||
| * 请求数据后,直接开始跳转 | |||
| */ | |||
| private fun loadLessonAfterTest(lesson : Lesson){ | |||
| vm.loadTest(AppConstants.TEST_TYPE_AFTER,lesson).observe(this){ | |||
| startLessonTest(lesson,AppConstants.TEST_TYPE_AFTER,it) | |||
| 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>){ | |||
| private fun startLessonTest(lesson : Lesson, examType : Int, testData : List<ExamBean>) { | |||
| //生成数据 | |||
| val examData = ExamData(vm.course.subjectId, examType, | |||
| lesson.lessonName, | |||
| 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 | |||
| @@ -212,29 +298,30 @@ class CourseLessonFragment : BaseFragmentVM<FragmentCourseLessonBinding, CourseM | |||
| courseType = vm.course.courseType | |||
| this.lesson = lesson | |||
| this.testData = testData | |||
| mExamWRMap = vm.courseDetail.exam_w_r_list | |||
| mExamWRMap = vm.courseDetail.exam_w_r_list | |||
| } | |||
| (this@CourseLessonFragment.parentFragment as CourseMainFragment).startExam(examData) | |||
| } | |||
| /** lesson的学习 lessonType 为 word类型 */ | |||
| private fun startLessonLearnForWord(){ | |||
| private fun startLessonLearnForWord() { | |||
| } | |||
| /** | |||
| * 当前课时学前、学习、学后都完成了的弹窗 | |||
| */ | |||
| private fun showLessonAllOverDialog(lesson : Lesson){ | |||
| private fun showLessonAllOverDialog(lesson : Lesson) { | |||
| } | |||
| /** | |||
| * 显示课程学习完成的弹窗 | |||
| */ | |||
| private fun showCourseOverDialog(){ | |||
| private fun showCourseOverDialog() { | |||
| // TODO: 2022/4/29 课程学习完成弹窗 | |||
| } | |||
| } | |||
| @@ -2,7 +2,9 @@ package com.xkl.cdl.module.m_center_learn.coursechildren | |||
| import androidx.lifecycle.MutableLiveData | |||
| import com.suliang.common.base.viewmodel.BaseViewModel | |||
| import com.suliang.common.extension.diskIo2DiskIo | |||
| import com.suliang.common.extension.diskIo2Main | |||
| import com.suliang.common.util.DateUtil | |||
| import com.xkl.cdl.data.bean.LearnWord | |||
| import com.xkl.cdl.data.bean.course.Course | |||
| import com.xkl.cdl.data.bean.course.CourseDetail | |||
| @@ -15,6 +17,7 @@ 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.functions.BiFunction | |||
| import mqComsumerV1.Struct | |||
| import java.util.* | |||
| import kotlin.collections.HashMap | |||
| @@ -131,5 +134,24 @@ class CourseMainFragmentViewModel(val courseIndex : Int) : BaseViewModel() { | |||
| return result | |||
| } | |||
| /** | |||
| * 保存课程与项目的进度 | |||
| * @param courseProgress Double 课程进度 | |||
| * @param subjectProgress Double 项目进度 | |||
| */ | |||
| fun updateLearnSchedule(courseProgress : Double, subjectProgress : Double) { | |||
| Observable.fromCallable { | |||
| val record = Struct.Record.newBuilder().addSchedule(Struct.LearnSchedule.newBuilder().apply { | |||
| projectId = course.subjectId.toLong() | |||
| packId = course.coursePackId | |||
| courseId = course.courseId | |||
| schedule = courseProgress.toFloat() | |||
| totalSchedule = subjectProgress.toFloat() | |||
| created = DateUtil.format(System.currentTimeMillis(), DateUtil.FORMAT_1) | |||
| }) | |||
| return@fromCallable DataRepository.saveRecord(record) | |||
| }.compose(diskIo2DiskIo()).subscribe() | |||
| } | |||
| } | |||
| @@ -12,12 +12,12 @@ import java.util.* | |||
| * @property isSpellModel 是否是拼写模式 | |||
| * @property examErrorsMap 学前总和章前错误列表 | |||
| */ | |||
| class LearnRuleUtil<T : BaseWord> constructor(val originLearnList : List<T>, | |||
| class LearnRuleUtil<T : BaseWord> constructor(private val originLearnList : List<T>, | |||
| isSpellModel : Boolean, | |||
| val examErrorsMap : Map<String, Boolean>?) { | |||
| private val examErrorsMap : Map<String, Boolean>?) { | |||
| //错误时循环插入间隔 | |||
| private var insertInterval : IntArray = when { | |||
| val insertInterval : IntArray = when { | |||
| isSpellModel -> intArrayOf(1, 2, 3, 5) | |||
| else -> intArrayOf(1, 2, 3, 5, 7, 10, 15) | |||
| } | |||
| @@ -35,11 +35,11 @@ class LearnRuleUtil<T : BaseWord> constructor(val originLearnList : List<T>, | |||
| private var currentIsError = false | |||
| //学习错误的集合: 本次点击的新错误(不包含 examErrorsMap 中的错误) | |||
| var currentErrorMap : MutableMap<String, Boolean> = HashMap() | |||
| var currentErrorMap : MutableMap<String, Boolean> = hashMapOf() | |||
| private set | |||
| //学习正确的集合: 存放学习中第一次正确的单词 | |||
| var currentCorrectMap : MutableMap<String, Boolean> = HashMap() | |||
| var currentCorrectMap : MutableMap<String, Boolean> = hashMapOf() | |||
| private set | |||
| /** | |||
| @@ -285,7 +285,7 @@ class LearnRuleUtil<T : BaseWord> constructor(val originLearnList : List<T>, | |||
| * @throws ClassNotFoundException | |||
| </T> */ | |||
| @Throws(IOException::class, ClassNotFoundException::class) | |||
| fun <T> copy(old : T) : T { | |||
| private fun <T> copy(old : T) : T { | |||
| // 写入字节流 | |||
| val baos = ByteArrayOutputStream() | |||
| val oos = ObjectOutputStream(baos) | |||
| @@ -0,0 +1,106 @@ | |||
| package com.xkl.cdl.util | |||
| import android.text.Html | |||
| import android.text.Spanned | |||
| import android.widget.TextView | |||
| import androidx.annotation.ColorInt | |||
| import com.suliang.common.util.ColorUtil | |||
| import com.suliang.common.util.os.ScreenUtil | |||
| import java.util.regex.Pattern | |||
| /** | |||
| * author suliang | |||
| * create 2022/4/27 14:49 | |||
| * Describe: 进行一些特殊设置 | |||
| */ | |||
| class ViewUtil { | |||
| companion object { | |||
| /** | |||
| * 生成显示用的音标 | |||
| * @param view TextView 用于显示的 textView | |||
| * @param phone_uk String? 英式音标 | |||
| * @param phone_us String? 美式音标 | |||
| * @return String? | |||
| */ | |||
| fun generatePhonetic(view:TextView , phone_uk : String?, phone_us : String?) : String? { | |||
| val maxWidth = ScreenUtil.getScreenWidth() - ScreenUtil.dp2px(48f) | |||
| return when { | |||
| //相等 | |||
| phone_uk == phone_us -> phone_uk | |||
| //都不为空 | |||
| !phone_uk.isNullOrEmpty() && !phone_us.isNullOrEmpty() -> { | |||
| "英 $phone_uk 美 $phone_us".let { | |||
| if (view.paint.measureText(it) > maxWidth) | |||
| "英 $phone_uk \n 美 $phone_us" | |||
| else it | |||
| } | |||
| } | |||
| !phone_uk.isNullOrEmpty() -> phone_uk | |||
| !phone_us.isNullOrEmpty() -> phone_us | |||
| else -> "" | |||
| } | |||
| } | |||
| /** | |||
| * 识字课程认读显示转换: (你)好,括号中的字变大,并设置颜色 | |||
| * @param word 处理的文字 | |||
| * @param colorId 处理的颜色id color.xml | |||
| * @return Spanned 你好 你为大写 | |||
| */ | |||
| fun literacyToHtmlWord(word : String, @ColorInt colorId : Int) : Spanned { | |||
| val m = Pattern.compile("\\([\u4e00-\u9fa5]\\)").matcher(word) | |||
| val builder = StringBuffer() | |||
| val color : String = ColorUtil.getHexWebString(colorId) | |||
| var isFind = false | |||
| var previousEndPosition = 0 //下次循环开始的位置 | |||
| while (m.find()) { | |||
| isFind = true | |||
| val startPosition = m.start() //左括号的下一位置 | |||
| val endPosition = m.end() //右括号位置 | |||
| if (startPosition - previousEndPosition > 0) { | |||
| builder.append("<font color=\"#FF323233\">").append(word.substring(previousEndPosition, startPosition)) | |||
| .append("</font>") | |||
| } | |||
| //直接匹配添加 | |||
| builder.append("<font color= \"").append(color).append("\"><big><big>") | |||
| .append(word.substring(startPosition + 1, endPosition - 1)).append("</big></big></font>") | |||
| previousEndPosition = endPosition | |||
| } | |||
| if (isFind) { | |||
| if (previousEndPosition != word.length) { | |||
| builder.append("<font color=\"#FF323233\">").append(word.substring(previousEndPosition)).append("</font>") | |||
| } | |||
| } else { | |||
| builder.append("<font color=\"").append(color).append("\">").append(word).append("</font>") | |||
| } | |||
| println(builder.toString()) | |||
| return Html.fromHtml(builder.toString()) | |||
| } | |||
| /** | |||
| * 识字课程,获取括号包裹的中文内容 (你)好 --> 你 | |||
| * @param word 识字的词语 | |||
| * @return 英文括号中的内容 | |||
| */ | |||
| fun literacyGetWord(word : String) : String { | |||
| val m = Pattern.compile("\\([\u4e00-\u9fa5]\\)").matcher(word) | |||
| return if (m.find()) { | |||
| word.substring(m.start() + 1, m.end() - 1) | |||
| } else word | |||
| } | |||
| /** 识字课程,备忘本使用,去掉括号包裹内容的中文括号 */ | |||
| fun literacyGetMemoWord(word : String) : String { | |||
| val m = Pattern.compile("\\([\u4e00-\u9fa5]\\)").matcher(word) | |||
| val buffer = StringBuffer() | |||
| while (m.find()) { | |||
| val startPosition = m.start() //左括号的下一位置 | |||
| val endPosition = m.end() //右括号位置 | |||
| m.appendReplacement(buffer, word.substring(startPosition + 1, endPosition - 1)) | |||
| } | |||
| m.appendTail(buffer) | |||
| return buffer.toString() | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,95 @@ | |||
| package com.xkl.cdl.widget; | |||
| import android.content.Context; | |||
| import android.graphics.Canvas; | |||
| import android.graphics.Paint; | |||
| import android.graphics.Path; | |||
| import android.util.AttributeSet; | |||
| import android.view.View; | |||
| import android.widget.LinearLayout; | |||
| import androidx.annotation.Nullable; | |||
| import androidx.core.content.ContextCompat; | |||
| import com.suliang.common.util.os.ScreenUtil; | |||
| import com.xkl.cdl.R; | |||
| /** | |||
| * author suliang | |||
| * create 2021/3/3 13:43 | |||
| * Describe: | |||
| */ | |||
| public class SpellTipsLinearLayout extends LinearLayout { | |||
| private Paint mPaint ; | |||
| private Path mPath ; | |||
| private float leftRightPointWidth ; | |||
| public SpellTipsLinearLayout(Context context) { | |||
| super(context); | |||
| init(); | |||
| } | |||
| public SpellTipsLinearLayout(Context context, @Nullable AttributeSet attrs) { | |||
| super(context, attrs); | |||
| init(); | |||
| } | |||
| public SpellTipsLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { | |||
| super(context, attrs, defStyleAttr); | |||
| init(); | |||
| } | |||
| private void init(){ | |||
| float lineWidth = ScreenUtil.INSTANCE.dp2px(1f); | |||
| leftRightPointWidth = ScreenUtil.INSTANCE.dp2px(20f); | |||
| mPaint = new Paint() ; | |||
| mPaint.setAntiAlias(true); | |||
| mPaint.setStyle(Paint.Style.STROKE); | |||
| mPaint.setStrokeWidth(lineWidth); | |||
| mPaint.setColor(ContextCompat.getColor(getContext(),R.color.gray_1)); | |||
| } | |||
| @Override | |||
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |||
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); | |||
| } | |||
| @Override | |||
| public void draw(Canvas canvas) { | |||
| super.draw(canvas); | |||
| } | |||
| @Override | |||
| protected void onDraw(Canvas canvas) { | |||
| super.onDraw(canvas); | |||
| if (mPath == null){ | |||
| mPath = new Path() ; | |||
| int totalWidth = getWidth(); | |||
| int totalHeight = getHeight(); | |||
| View childOne = getChildAt(0); | |||
| int width = childOne.getWidth(); | |||
| int height = childOne.getHeight(); | |||
| int topHalf = (totalWidth - width) / 2; //顶部半条线长度 | |||
| mPath.moveTo(0,height); | |||
| mPath.lineTo(topHalf, height); | |||
| mPath.lineTo(topHalf + leftRightPointWidth , 0 ); | |||
| mPath.lineTo(topHalf + width - leftRightPointWidth , 0); | |||
| mPath.lineTo(topHalf + width , height); | |||
| mPath.lineTo(totalWidth, height); | |||
| mPath.lineTo(totalWidth,totalHeight); | |||
| mPath.lineTo(0,totalHeight); | |||
| mPath.close(); | |||
| } | |||
| canvas.drawPath(mPath,mPaint); | |||
| } | |||
| } | |||
| @@ -95,6 +95,7 @@ class VoiceSwitch @JvmOverloads constructor(context : Context, attr : AttributeS | |||
| /** 设置默认发音 */ | |||
| fun setSoundWay(way : Int){ | |||
| soundWay = way | |||
| soundWayChange.value = soundWay | |||
| invalidate() | |||
| } | |||
| } | |||
| @@ -37,7 +37,6 @@ | |||
| tools:reverseLayout="true" | |||
| tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" | |||
| tools:itemCount="1" | |||
| android:onItemSelected="1" | |||
| app:layout_constraintHorizontal_bias="0" | |||
| tools:listitem="@layout/item_historical_route" | |||
| /> | |||
| @@ -83,22 +82,38 @@ | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@id/container_layout" /> | |||
| <!--详情--> | |||
| <include | |||
| android:id="@+id/inc_detail" | |||
| layout="@layout/inc_word_detail" | |||
| <View | |||
| android:id="@+id/v_space" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="@dimen/line_height" | |||
| android:background="@color/gray_1" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/inc_control_button" /> | |||
| <FrameLayout | |||
| android:id="@+id/detail_layout" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="0dp" | |||
| app:layout_constraintBottom_toTopOf="@+id/tv_use_total_time" | |||
| app:layout_constraintBottom_toTopOf="@+id/tv_valid_time" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/inc_control_button" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| app:layout_constraintTop_toBottomOf="@+id/v_space"> | |||
| <!--详情--> | |||
| <!-- <include layout="@layout/inc_word_detail" />--> | |||
| <!--拼写提示--> | |||
| <!-- <include layout="@layout/inc_spell_learn_tip" />--> | |||
| </FrameLayout> | |||
| <!--总时间--> | |||
| <TextView | |||
| android:id="@+id/tv_use_total_time" | |||
| android:id="@+id/tv_valid_time" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="30dp" | |||
| android:gravity="center" | |||
| @@ -110,6 +125,7 @@ | |||
| app:layout_constraintEnd_toEndOf="parent"/> | |||
| <Button | |||
| android:id="@+id/tv_play" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="22dp" | |||
| style="@style/Widget.MaterialComponents.Button.UnelevatedButton" | |||
| @@ -130,6 +146,7 @@ | |||
| app:rippleColor="@color/white"/> | |||
| <Button | |||
| android:id="@+id/tv_play_stop" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="22dp" | |||
| style="@style/Widget.MaterialComponents.Button.UnelevatedButton" | |||
| @@ -126,6 +126,33 @@ | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_tip_1" | |||
| android:visibility="gone"/> | |||
| <TextView | |||
| android:id="@+id/tv_learn_over_tip" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginTop="12dp" | |||
| app:layout_goneMarginTop="80dp" | |||
| android:textColor="@color/main_text_color" | |||
| android:textSize="@dimen/smallSize" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/inc_statistics_number" | |||
| android:text="我的学习效果是?快去课时学后测试吧!" | |||
| android:visibility="gone"/> | |||
| <TextView | |||
| android:id="@+id/tv_learn_over_for_after_count_time" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginTop="4dp" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallerSize" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_learn_over_tip" | |||
| tools:text="@string/test_count_time_format" | |||
| android:visibility="gone"/> | |||
| <TextView | |||
| android:id="@+id/tv_top" | |||
| android:layout_width="wrap_content" | |||
| @@ -137,7 +164,7 @@ | |||
| android:textSize="@dimen/smallSize" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@id/inc_statistics_number" | |||
| app:layout_constraintTop_toBottomOf="@id/tv_learn_over_for_after_count_time" | |||
| android:visibility="gone" | |||
| /> | |||
| <TextView | |||
| @@ -214,7 +241,18 @@ | |||
| app:layout_constraintBottom_toBottomOf="@+id/vSplit" | |||
| android:visibility="gone" | |||
| /> | |||
| <!--所有布局id--> | |||
| <!-- <androidx.constraintlayout.widget.Group--> | |||
| <!-- android:layout_width="wrap_content"--> | |||
| <!-- android:layout_height="wrap_content"--> | |||
| <!-- app:constraint_referenced_ids="iv_close,tv_score,tv_tip,tv_title,--> | |||
| <!-- tv_lesson_name,tv_count_time,,tv_tip_1,--> | |||
| <!-- inc_statistics_number,--> | |||
| <!-- tv_learn_over_tip,--> | |||
| <!-- tv_learn_over_for_after_count_time,--> | |||
| <!-- tv_top,tv_top_1,tv_left,vSplit"--> | |||
| <!-- tools:visibility="gone"--> | |||
| <!-- />--> | |||
| <!-- <!–学前总测试控制显示的布局id–>--> | |||
| <!-- <androidx.constraintlayout.widget.Group--> | |||
| <!-- android:id="@+id/group_total_test"--> | |||
| @@ -237,13 +275,23 @@ | |||
| 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 | |||
| /> --> | |||
| <!--课时正常学习结束的布局组 --> | |||
| <!-- <androidx.constraintlayout.widget.Group--> | |||
| <!-- android:layout_width="wrap_content"--> | |||
| <!-- android:layout_height="wrap_content"--> | |||
| <!-- app:constraint_referenced_ids="tv_title,inc_statistics_number,tv_learn_over_tip,tv_learn_over_for_after_count_time,tv_left,vSplit"/>--> | |||
| <!--课时学后测试结束绑定的布局组 --> | |||
| <!-- <androidx.constraintlayout.widget.Group | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| 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> | |||
| @@ -11,14 +11,16 @@ | |||
| android:id="@+id/iv_voice" | |||
| android:layout_width="74dp" | |||
| android:layout_height="74dp" | |||
| app:lottie_fileName="data.json" | |||
| app:lottie_fileName="voice.json" | |||
| app:lottie_loop="true" | |||
| app:lottie_autoPlay="false" | |||
| app:lottie_repeatMode="restart" | |||
| app:layout_constraintLeft_toLeftOf="parent" | |||
| app:layout_constraintRight_toRightOf="parent" | |||
| app:layout_constraintTop_toTopOf="parent" | |||
| app:layout_constraintBottom_toBottomOf="parent" /> | |||
| app:layout_constraintBottom_toBottomOf="parent" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| <!--图片--> | |||
| <com.google.android.material.imageview.ShapeableImageView | |||
| @@ -31,7 +33,9 @@ | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toTopOf="parent" | |||
| app:shapeAppearance="@style/roundedCornerStyle" | |||
| tools:src="@mipmap/img_default" /> | |||
| tools:src="@mipmap/img_default" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| <include | |||
| android:id="@+id/inc_word" | |||
| @@ -40,20 +44,24 @@ | |||
| android:layout_height="@dimen/height_word" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/img_word" /> | |||
| app:layout_constraintTop_toBottomOf="@+id/img_word" | |||
| android:visibility="gone" | |||
| tools:visibility="visible" /> | |||
| <TextView | |||
| android:id="@+id/tv_phonetic" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginTop="10dp" | |||
| android:layout_marginTop="4dp" | |||
| android:gravity="center" | |||
| android:paddingStart="@dimen/global_spacing" | |||
| android:paddingRight="@dimen/global_spacing" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallerSize" | |||
| app:layout_constraintTop_toBottomOf="@+id/inc_word" | |||
| tools:text="英 [gud:bai] 美 [gud:bai] " /> | |||
| tools:text="英 [gud:bai] 美 [gud:bai] " | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| <TextView | |||
| android:id="@+id/tv_explain" | |||
| @@ -67,7 +75,9 @@ | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_phonetic" | |||
| tools:text="基本释义" /> | |||
| tools:text="基本释义" | |||
| android:visibility="gone" | |||
| tools:visibility="visible" /> | |||
| <TextView | |||
| android:id="@+id/tv_expand_explain" | |||
| @@ -81,7 +91,9 @@ | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_explain" | |||
| tools:text="扩展释义>" /> | |||
| tools:text="扩展释义>" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| <TextView | |||
| android:id="@+id/tv_pattern" | |||
| @@ -95,6 +107,8 @@ | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_expand_explain" | |||
| tools:text="句型>" /> | |||
| tools:text="句型>" | |||
| android:visibility="gone" | |||
| tools:visibility="visible"/> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| @@ -0,0 +1,40 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <com.xkl.cdl.widget.SpellTipsLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| xmlns:tools="http://schemas.android.com/tools" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginLeft="38dp" | |||
| android:layout_marginTop="20dp" | |||
| android:layout_marginRight="38dp" | |||
| android:background="@color/white" | |||
| android:orientation="vertical" | |||
| > | |||
| <!-- tools:showIn="@layout/activity_learn_word"--> | |||
| <TextView | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:layout_gravity="center_horizontal" | |||
| android:gravity="center" | |||
| android:paddingLeft="40dp" | |||
| android:paddingTop="8dp" | |||
| android:paddingRight="40dp" | |||
| android:paddingBottom="8dp" | |||
| android:text="操作提示" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallerSize" /> | |||
| <TextView | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content" | |||
| android:layout_gravity="center_horizontal" | |||
| android:gravity="center|start" | |||
| android:paddingLeft="10dp" | |||
| android:paddingTop="14dp" | |||
| android:paddingRight="10dp" | |||
| android:paddingBottom="14dp" | |||
| android:text="1.上下两个字母中有一个是正确的, 请按顺序点击正确的即可\n2.点击中文题目可以重读单词\n3.如果选错了,需要点击一下正确的字母,方可进入下一条" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallerSize" /> | |||
| </com.xkl.cdl.widget.SpellTipsLinearLayout> | |||
| @@ -10,16 +10,6 @@ | |||
| android:layout_width="match_parent" | |||
| android:layout_height="match_parent"> | |||
| <View | |||
| android:id="@+id/v_space" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="@dimen/line_height" | |||
| android:background="@color/gray_1" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toTopOf="parent" /> | |||
| <TextView | |||
| android:id="@+id/tv_phrase_flag" | |||
| android:layout_width="wrap_content" | |||
| @@ -34,7 +24,7 @@ | |||
| android:textStyle="bold" | |||
| app:drawableStartCompat="@drawable/detail_phrase_flag" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/v_space"/> | |||
| app:layout_constraintTop_toTopOf="parent"/> | |||
| <com.suliang.common.widget.TagLinearLayout | |||
| android:id="@+id/layout_phrase" | |||
| @@ -73,5 +73,7 @@ | |||
| <string name="example">例句</string> | |||
| <string name="reference">参考</string> | |||
| <string name="auto_playing">自动播放中···</string> | |||
| <string name="quit_learn_title">你确定要退出本课程的学习吗?</string> | |||
| <string name="quit_learn_content">退出后系统将保存你的学习进度</string> | |||
| </resources> | |||
| @@ -111,7 +111,7 @@ abstract class BaseRVAdapter<T> : | |||
| * @param data T? | |||
| * @param position Int? | |||
| */ | |||
| open fun addData(data: T?, position: Int?) { | |||
| open fun addData(data: T?, position: Int? = null) { | |||
| data?.let { | |||
| val size = mData.size | |||
| val startPosition = position?.let { it -> | |||
| @@ -15,67 +15,7 @@ class StringUtil { | |||
| companion object { | |||
| /** | |||
| * 识字课程认读显示转换: (你)好,括号中的字变大,并设置颜色 | |||
| * @param word 处理的文字 | |||
| * @param colorId 处理的颜色id color.xml | |||
| * @return | |||
| */ | |||
| fun literacyToHtmlWord(word : String, @ColorInt colorId : Int) : Spanned? { | |||
| val m = Pattern.compile("\\([\u4e00-\u9fa5]\\)").matcher(word) | |||
| val builder = StringBuffer() | |||
| val color : String = ColorUtil.getHexWebString(colorId) | |||
| var isFind = false | |||
| var previousEndPosition = 0 //下次循环开始的位置 | |||
| while (m.find()) { | |||
| isFind = true | |||
| val startPosition = m.start() //左括号的下一位置 | |||
| val endPosition = m.end() //右括号位置 | |||
| if (startPosition - previousEndPosition > 0) { | |||
| builder.append("<font color=\"#FF323233\">").append(word.substring(previousEndPosition, startPosition)) | |||
| .append("</font>") | |||
| } | |||
| //直接匹配添加 | |||
| builder.append("<font color= \"").append(color).append("\"><big><big>") | |||
| .append(word.substring(startPosition + 1, endPosition - 1)).append("</big></big></font>") | |||
| previousEndPosition = endPosition | |||
| } | |||
| if (isFind) { | |||
| if (previousEndPosition != word.length) { | |||
| builder.append("<font color=\"#FF323233\">").append(word.substring(previousEndPosition)).append("</font>") | |||
| } | |||
| } else { | |||
| builder.append("<font color=\"").append(color).append("\">").append(word).append("</font>") | |||
| } | |||
| println(builder.toString()) | |||
| return Html.fromHtml(builder.toString()) | |||
| } | |||
| /** | |||
| * 识字课程,获取括号包裹的中文内容 (你)好 --> 你 | |||
| * @param word 识字的词语 | |||
| * @return 英文括号中的内容 | |||
| */ | |||
| fun literacyGetWord(word : String) : String { | |||
| val m = Pattern.compile("\\([\u4e00-\u9fa5]\\)").matcher(word) | |||
| return if (m.find()) { | |||
| word.substring(m.start() + 1, m.end() - 1) | |||
| } else word | |||
| } | |||
| /** 识字课程,备忘本使用,去掉括号包裹内容的中文括号 */ | |||
| fun literacyGetMemoWord(word : String) : String { | |||
| val m = Pattern.compile("\\([\u4e00-\u9fa5]\\)").matcher(word) | |||
| val buffer = StringBuffer() | |||
| while (m.find()) { | |||
| val startPosition = m.start() //左括号的下一位置 | |||
| val endPosition = m.end() //右括号位置 | |||
| m.appendReplacement(buffer, word.substring(startPosition + 1, endPosition - 1)) | |||
| } | |||
| m.appendTail(buffer) | |||
| return buffer.toString() | |||
| } | |||
| } | |||