Browse Source

单词课程测试、学习功能实现

master
suliang 2 years ago
parent
commit
dd0982c82e
33 changed files with 1751 additions and 399 deletions
  1. 8
    0
      .idea/codeStyles/Project.xml
  2. 2
    1
      .idea/misc.xml
  3. 2
    0
      ProjectErrors.md
  4. 2
    1
      app/src/main/java/com/xkl/cdl/Extension.kt
  5. 14
    8
      app/src/main/java/com/xkl/cdl/adapter/AdapterHistoricalRoute.kt
  6. 159
    142
      app/src/main/java/com/xkl/cdl/adapter/AdapterSpell.kt
  7. 5
    3
      app/src/main/java/com/xkl/cdl/data/AppConstants.kt
  8. 2
    1
      app/src/main/java/com/xkl/cdl/data/bean/BaseWord.kt
  9. 0
    3
      app/src/main/java/com/xkl/cdl/data/bean/LearnWord.kt
  10. 3
    0
      app/src/main/java/com/xkl/cdl/data/bean/intentdata/LearnData.kt
  11. 2
    0
      app/src/main/java/com/xkl/cdl/data/event/LearnEventData.kt
  12. 132
    12
      app/src/main/java/com/xkl/cdl/data/manager/CourseManager.kt
  13. 48
    2
      app/src/main/java/com/xkl/cdl/data/repository/DataRepository.kt
  14. 28
    6
      app/src/main/java/com/xkl/cdl/dialog/LearnDialog.kt
  15. 33
    7
      app/src/main/java/com/xkl/cdl/module/learn/LearnBaseViewModel.kt
  16. 2
    3
      app/src/main/java/com/xkl/cdl/module/learn/LearnExamActivity.kt
  17. 4
    4
      app/src/main/java/com/xkl/cdl/module/learn/LearnExamViewModel.kt
  18. 497
    43
      app/src/main/java/com/xkl/cdl/module/learn/LearnWordActivity.kt
  19. 286
    3
      app/src/main/java/com/xkl/cdl/module/learn/LearnWordViewModel.kt
  20. 143
    56
      app/src/main/java/com/xkl/cdl/module/m_center_learn/coursechildren/CourseLessonFragment.kt
  21. 22
    0
      app/src/main/java/com/xkl/cdl/module/m_center_learn/coursechildren/CourseMainFragmentViewModel.kt
  22. 6
    6
      app/src/main/java/com/xkl/cdl/util/LearnRuleUtil.kt
  23. 106
    0
      app/src/main/java/com/xkl/cdl/util/ViewUtil.kt
  24. 95
    0
      app/src/main/java/com/xkl/cdl/widget/SpellTipsLinearLayout.java
  25. 1
    0
      app/src/main/java/com/xkl/cdl/widget/VoiceSwitch.kt
  26. 27
    10
      app/src/main/res/layout/activity_learn_word.xml
  27. 54
    6
      app/src/main/res/layout/dialog_lesson_learn.xml
  28. 23
    9
      app/src/main/res/layout/inc_learn_word.xml
  29. 40
    0
      app/src/main/res/layout/inc_spell_learn_tip.xml
  30. 1
    11
      app/src/main/res/layout/inc_word_detail.xml
  31. 2
    0
      app/src/main/res/values/strings.xml
  32. 1
    1
      lib/common/src/main/java/com/suliang/common/base/adapter/BaseRVAdapter.kt
  33. 1
    61
      lib/common/src/main/java/com/suliang/common/util/StringUtil.kt

+ 8
- 0
.idea/codeStyles/Project.xml View File

@@ -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" />

+ 2
- 1
.idea/misc.xml View File

@@ -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" />

+ 2
- 0
ProjectErrors.md View File

@@ -47,3 +47,5 @@ google 给与的唯一标识符最佳做法:

如何实现防快速点击 aspectJ

SpellTipsLinearLayout onDraw为什么不调用?为什么设置背景后就有效过了?


+ 2
- 1
app/src/main/java/com/xkl/cdl/Extension.kt View File

@@ -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

+ 14
- 8
app/src/main/java/com/xkl/cdl/adapter/AdapterHistoricalRoute.kt View File

@@ -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
//添加数据拼写辨音初始值

+ 159
- 142
app/src/main/java/com/xkl/cdl/adapter/AdapterSpell.kt View File

@@ -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")
}
}
}

+ 5
- 3
app/src/main/java/com/xkl/cdl/data/AppConstants.kt View File

@@ -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
/**课时学后测试弹窗动作: 下一步*/

+ 2
- 1
app/src/main/java/com/xkl/cdl/data/bean/BaseWord.kt View File

@@ -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


+ 0
- 3
app/src/main/java/com/xkl/cdl/data/bean/LearnWord.kt View File

@@ -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 //词组

+ 3
- 0
app/src/main/java/com/xkl/cdl/data/bean/intentdata/LearnData.kt View File

@@ -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()

+ 2
- 0
app/src/main/java/com/xkl/cdl/data/event/LearnEventData.kt View File

@@ -18,4 +18,6 @@ class LearnEventData(val subjectId : Int, var courseId : Long, val actionFlag :
//课时学前测试 除了学前总测传递数据,还需要课时位置数据,
var leesonPositionIndex = 0 // lesson所在位置
//学习结束 newErrorMap 保存为所有的错误,包含学前和课程前的测试,主要用与后面进行小游戏的数据加载
}

+ 132
- 12
app/src/main/java/com/xkl/cdl/data/manager/CourseManager.kt View File

@@ -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
}
}

+ 48
- 2
app/src/main/java/com/xkl/cdl/data/repository/DataRepository.kt View File

@@ -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) {
}
}

+ 28
- 6
app/src/main/java/com/xkl/cdl/dialog/LearnDialog.kt View File

@@ -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)

+ 33
- 7
app/src/main/java/com/xkl/cdl/module/learn/LearnBaseViewModel.kt View File

@@ -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
}
}

+ 2
- 3
app/src/main/java/com/xkl/cdl/module/learn/LearnExamActivity.kt View File

@@ -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,

+ 4
- 4
app/src/main/java/com/xkl/cdl/module/learn/LearnExamViewModel.kt View File

@@ -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() {
}
}
}
}
}
}

+ 497
- 43
app/src/main/java/com/xkl/cdl/module/learn/LearnWordActivity.kt View File

@@ -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")
}
}

+ 286
- 3
app/src/main/java/com/xkl/cdl/module/learn/LearnWordViewModel.kt View File

@@ -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()
}
}

+ 143
- 56
app/src/main/java/com/xkl/cdl/module/m_center_learn/coursechildren/CourseLessonFragment.kt View File

@@ -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 课程学习完成弹窗
}
}

+ 22
- 0
app/src/main/java/com/xkl/cdl/module/m_center_learn/coursechildren/CourseMainFragmentViewModel.kt View File

@@ -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()
}
}

+ 6
- 6
app/src/main/java/com/xkl/cdl/util/LearnRuleUtil.kt View File

@@ -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)

+ 106
- 0
app/src/main/java/com/xkl/cdl/util/ViewUtil.kt View File

@@ -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()
}
}
}

+ 95
- 0
app/src/main/java/com/xkl/cdl/widget/SpellTipsLinearLayout.java View File

@@ -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);

}

}

+ 1
- 0
app/src/main/java/com/xkl/cdl/widget/VoiceSwitch.kt View File

@@ -95,6 +95,7 @@ class VoiceSwitch @JvmOverloads constructor(context : Context, attr : AttributeS
/** 设置默认发音 */
fun setSoundWay(way : Int){
soundWay = way
soundWayChange.value = soundWay
invalidate()
}
}

+ 27
- 10
app/src/main/res/layout/activity_learn_word.xml View File

@@ -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"

+ 54
- 6
app/src/main/res/layout/dialog_lesson_learn.xml View File

@@ -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"-->
<!-- />-->
<!-- &lt;!&ndash;学前总测试控制显示的布局id&ndash;&gt;-->
<!-- <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>


+ 23
- 9
app/src/main/res/layout/inc_learn_word.xml View File

@@ -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>

+ 40
- 0
app/src/main/res/layout/inc_spell_learn_tip.xml View File

@@ -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>

+ 1
- 11
app/src/main/res/layout/inc_word_detail.xml View File

@@ -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"

+ 2
- 0
app/src/main/res/values/strings.xml View File

@@ -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>

+ 1
- 1
lib/common/src/main/java/com/suliang/common/base/adapter/BaseRVAdapter.kt View File

@@ -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 ->

+ 1
- 61
lib/common/src/main/java/com/suliang/common/util/StringUtil.kt View File

@@ -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()
}
}

Loading…
Cancel
Save