| @@ -130,7 +130,7 @@ | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/item_checkbox.xml" value="1.0" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/item_course_lesson.xml" value="0.4785615491009682" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/item_dic_detail_chinese.xml" value="0.33242753623188404" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/item_dic_detail_chinese_for_english_item.xml" value="0.75" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/item_dic_detail_chinese_for_english_item.xml" value="0.6032138442521632" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/item_dic_detail_english.xml" value="0.5" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/item_dic_history_chinese.xml" value="0.23632218844984804" /> | |||
| <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/item_dic_history_english.xml" value="0.23632218844984804" /> | |||
| @@ -16,10 +16,20 @@ android { | |||
| versionName androidConfig.version_name | |||
| multiDexEnabled true //解决64k 分包限制 | |||
| testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | |||
| javaCompileOptions { | |||
| annotationProcessorOptions { | |||
| arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] | |||
| //java环境roomSchemaLocation配置 | |||
| // javaCompileOptions { | |||
| // annotationProcessorOptions { | |||
| // arguments = [ | |||
| // "room.schemaLocation" : "$projectDir/schemas".toString(), | |||
| // "room.incremental" : "true", | |||
| // "room.expandProjection" :"true" | |||
| // ] | |||
| // } | |||
| // } | |||
| //Kotlin环境RoomSchemaLocation配置 | |||
| kapt{ | |||
| arguments{ | |||
| arg("room.schemaLocation","$projectDir/schemas") | |||
| } | |||
| } | |||
| @@ -1,11 +1,24 @@ | |||
| package com.xkl.cdl.adapter | |||
| import android.view.View | |||
| import android.view.ViewGroup | |||
| import androidx.core.content.ContextCompat | |||
| import androidx.recyclerview.widget.LinearLayoutManager | |||
| import com.suliang.common.base.adapter.BaseAdapterViewHolder | |||
| import com.suliang.common.base.adapter.BaseRVAdapterVM | |||
| import com.suliang.common.databinding.ItemEmptyBinding | |||
| import com.suliang.common.extension.click | |||
| import com.suliang.common.util.StringUtil | |||
| import com.xkl.cdl.R | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.bean.DictionaryItem | |||
| import com.xkl.cdl.data.manager.UserInfoManager | |||
| import com.xkl.cdl.data.repository.AudioCache | |||
| import com.xkl.cdl.databinding.ItemDicDetailChineseBinding | |||
| import com.xkl.cdl.databinding.ItemDicDetailChineseForEnglishItemBinding | |||
| import com.xkl.cdl.databinding.ItemDicDetailEnglishBinding | |||
| import com.xkl.cdl.databinding.ItemDicSearchBinding | |||
| import com.xkl.cdl.initValue | |||
| import com.xkl.cdl.module.m_service_center.DictionaryViewModel | |||
| /** | |||
| @@ -13,71 +26,207 @@ import com.xkl.cdl.module.m_service_center.DictionaryViewModel | |||
| * create 2022/8/4 10:41 | |||
| * Describe: | |||
| */ | |||
| class DictionaryAdapter(vm:DictionaryViewModel) : BaseRVAdapterVM<DictionaryItem,DictionaryViewModel>(vm) { | |||
| class DictionaryAdapter(vm : DictionaryViewModel) : BaseRVAdapterVM<DictionaryItem, DictionaryViewModel>(vm) { | |||
| override fun getItemCount() : Int { | |||
| //历史记录,不显示空状态 | |||
| return if (vm.detailState == 1) { | |||
| //大于页数显示的总数量,则显示指定数量,否则显示全部 | |||
| val showCount = vm.histroyShowPage * vm.historyPageSize | |||
| if (getData().size >= showCount){ | |||
| showCount | |||
| }else{ | |||
| getData().size | |||
| /** | |||
| * 获取显示 viewType | |||
| * 1 如果允许显示空 则显示空 | |||
| * 2 历史记录列表和关键字搜索列表,展示type为 3 | |||
| * 3 详情 item type 为item的form 1英文 2中文 | |||
| * @param position Int | |||
| * @return Int | |||
| */ | |||
| override fun getItemViewType(position : Int) : Int { | |||
| return when { | |||
| enableEmptyPosition(position) -> TYPE_EMPTY | |||
| else -> when(vm.detailState){ | |||
| //历史记录和关键字搜索结果列表 | |||
| DictionaryViewModel.STATE_HISTROY,DictionaryViewModel.STATE_SEARCH -> { | |||
| 3 | |||
| } | |||
| //单条记录结果 | |||
| else -> getItem(position).form //详情展示 | |||
| } | |||
| }else if (needShowEmptyView && getData().isEmpty()) 1 else getData().size | |||
| } | |||
| } | |||
| override fun onBindEmptyViewHolder(holder : BaseAdapterViewHolder) { | |||
| (holder.binding as ItemEmptyBinding).run { | |||
| //根据监听,显示具体的内容 | |||
| vm.etSearchLiveData.value?.let { | |||
| if (it.isNotEmpty()) { | |||
| imgEmpty.setImageResource(R.mipmap.empty_nothing_search) | |||
| tvContent.text = "没有搜索到任何内容" | |||
| } else { | |||
| imgEmpty.setImageResource(R.mipmap.empty_nothing) | |||
| tvContent.text = "没有数据" | |||
| /** | |||
| * 空状态为 1 | |||
| * 列表状态根据可显示数量,进行显示 | |||
| * 详情就直接显示数量 | |||
| * @return Int | |||
| */ | |||
| override fun getItemCount() : Int { | |||
| return when { | |||
| //允许空显示,且数据为空时,显示空界面 | |||
| needShowEmptyView && getData().isEmpty() -> { | |||
| 1 | |||
| } | |||
| else -> when (vm.detailState) { | |||
| DictionaryViewModel.STATE_HISTROY -> { | |||
| val showCount = vm.histroyShowPage * vm.pageShowCount | |||
| if (getData().size >= showCount) { | |||
| showCount | |||
| } else { | |||
| getData().size | |||
| } | |||
| } | |||
| DictionaryViewModel.STATE_SEARCH -> { | |||
| val showCount = vm.searchShowPage * vm.pageShowCount | |||
| if (getData().size >= showCount) { | |||
| showCount | |||
| } else { | |||
| getData().size | |||
| } | |||
| } | |||
| //单条记录结果 | |||
| else -> { | |||
| getData().size | |||
| } | |||
| } | |||
| } | |||
| } | |||
| override fun getItemViewType(position : Int) : Int { | |||
| return when{ | |||
| enableEmptyPosition(position) -> TYPE_EMPTY | |||
| //单条详情 | |||
| vm.searchItemDetailAble -> { | |||
| val item = getItem(position) | |||
| item.form //1英文 2 中文 | |||
| } | |||
| //多条记录 | |||
| else -> 3 | |||
| /** | |||
| * 空状态显示 | |||
| * @param holder BaseAdapterViewHolder | |||
| */ | |||
| override fun onBindEmptyViewHolder(holder : BaseAdapterViewHolder) { | |||
| (holder.binding as ItemEmptyBinding).run { | |||
| imgEmpty.setImageResource(R.mipmap.empty_nothing_search) | |||
| tvContent.text = "没有搜索到任何内容" | |||
| } | |||
| } | |||
| override fun coverViewHolder(parent : ViewGroup, viewType : Int) : BaseAdapterViewHolder { | |||
| return when(viewType){ | |||
| 1 -> BaseAdapterViewHolder(inflateBinding(parent,R.layout.item_dic_detail_english)) //英文搜索结果详情 | |||
| 2 -> BaseAdapterViewHolder(inflateBinding(parent,R.layout.item_dic_detail_chinese)) //中文搜索结果详情 | |||
| else -> BaseAdapterViewHolder(inflateBinding(parent,R.layout.item_dic_search)) //记录列表 历史和搜索记录列表 | |||
| return when (viewType) { | |||
| 1 -> BaseAdapterViewHolder(inflateBinding(parent, R.layout.item_dic_detail_english)) //英文搜索结果详情 | |||
| 2 -> BaseAdapterViewHolder(inflateBinding(parent, R.layout.item_dic_detail_chinese)) //中文搜索结果详情 | |||
| else -> BaseAdapterViewHolder(inflateBinding(parent, R.layout.item_dic_search)) //记录列表 历史和搜索记录列表 | |||
| } | |||
| } | |||
| override fun onBindVH(holder : BaseAdapterViewHolder, position : Int) { | |||
| val itemViewType = getItemViewType(position) | |||
| when(itemViewType){ | |||
| when (itemViewType) { | |||
| //英文 | |||
| 1 -> { | |||
| getItem(position).let { | |||
| (holder.binding as ItemDicDetailEnglishBinding).run { | |||
| tvWord.text = it.word | |||
| when { | |||
| it.phonetic_uk.isNullOrEmpty() -> tvPhoneticUk.visibility = View.GONE | |||
| else -> tvPhoneticUk.apply { | |||
| visibility = View.VISIBLE | |||
| text = "英 ${it.phonetic_uk}" | |||
| click { v -> //发音 | |||
| AudioCache.getDictionaryVoice(it.id,AppConstants.SOUND_TYPE_UK) | |||
| } | |||
| } | |||
| } | |||
| when { | |||
| it.phonectic_us.isNullOrEmpty() -> tvPhoneticUs.visibility = View.GONE | |||
| else -> tvPhoneticUs.apply { | |||
| visibility = View.VISIBLE | |||
| text = "美 ${it.phonectic_us}" | |||
| click { v -> //发音 | |||
| AudioCache.getDictionaryVoice(it.id,AppConstants.SOUND_TYPE_US) | |||
| } | |||
| } | |||
| } | |||
| when { | |||
| it.basic_explaination.isNullOrEmpty() -> tvExplain.visibility = View.GONE | |||
| else -> tvExplain.apply { | |||
| visibility = View.VISIBLE | |||
| text = it.basic_explaination | |||
| } | |||
| } | |||
| when { | |||
| it.all_explaination.isNullOrEmpty() -> tvExpandExplain.visibility = View.GONE | |||
| else -> tvExpandExplain.apply { | |||
| visibility = View.VISIBLE | |||
| text = "扩展释义: ${it.all_explaination}" | |||
| } | |||
| } | |||
| incWordDetail.initValue(it.phrase, it.example, it.reference) | |||
| } | |||
| } | |||
| } | |||
| //中文 | |||
| 2 -> { | |||
| getItem(position).let { | |||
| (holder.binding as ItemDicDetailChineseBinding).run { | |||
| tvWord.text = it.word | |||
| when { | |||
| it.phonectic_cn.isNullOrEmpty() -> tvPhonetic.visibility = View.GONE | |||
| else -> tvPhonetic.apply { | |||
| visibility = View.VISIBLE | |||
| text = it.phonectic_cn | |||
| } | |||
| } | |||
| recyclerView.apply { | |||
| layoutManager = LinearLayoutManager(context,LinearLayoutManager.VERTICAL,false) | |||
| adapter = adapterChinese | |||
| } | |||
| //查询数据后更新显示 | |||
| vm.queryChineseForEnglishList(it) | |||
| } | |||
| } | |||
| } | |||
| //多条记录 搜索结果与历史记录 | |||
| 3 -> { | |||
| val binding = holder.binding as ItemDicSearchBinding | |||
| getItem(position).let { | |||
| binding.apply { | |||
| iv.setImageResource(if (it.inHistory) R.drawable.ic_clock else R.drawable.ic_search) | |||
| tvValue.text = it.basic_explaination | |||
| tvWord.text = when (vm.detailState) { | |||
| DictionaryViewModel.STATE_HISTROY -> it.word | |||
| DictionaryViewModel.STATE_SEARCH -> StringUtil.highString(it.word, | |||
| vm.keyWord, | |||
| ContextCompat.getColor(context, | |||
| R.color.theme_color)) | |||
| else -> "" | |||
| } | |||
| //点击,进入详情 | |||
| binding.root.click { v -> | |||
| vm.insertAndUpdateHistory(it, position) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| val adapterChinese = DicDetailChinese(vm) | |||
| class DicDetailChinese(_vm:DictionaryViewModel) : BaseRVAdapterVM<DictionaryItem,DictionaryViewModel>(_vm){ | |||
| override fun coverViewHolder(parent : ViewGroup, viewType : Int) : BaseAdapterViewHolder { | |||
| return BaseAdapterViewHolder(inflateBinding(parent,R.layout.item_dic_detail_chinese_for_english_item)) | |||
| } | |||
| override fun onBindVH(holder : BaseAdapterViewHolder, position : Int) { | |||
| getItem(position).let { | |||
| (holder.binding as ItemDicDetailChineseForEnglishItemBinding).apply { | |||
| tvValue.text = it.basic_explaination | |||
| tvWord.text = it.word | |||
| ivHoruns.click { v -> //发音 | |||
| AudioCache.getDictionaryVoice(it.id,UserInfoManager.getDefaultSoundWay()) | |||
| } | |||
| //点击,进入详情 | |||
| root.click { v -> | |||
| //更改或添加进入数据库,更新插入时间,更新历史记录列表位置 | |||
| vm.insertAndUpdateHistory(it, position) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -7,7 +7,7 @@ package com.xkl.cdl.data.bean | |||
| */ | |||
| open class DictionaryBean() { | |||
| var id : Long = 0 | |||
| var word : String? = null | |||
| var word : String = "" | |||
| var form : Int = 0 // 1 英文 2中文 | |||
| var pre_index : String? = null //前缀 | |||
| var phonetic_uk : String? = null | |||
| @@ -16,9 +16,11 @@ class DictionaryItem : DictionaryBean() { | |||
| //查询时间 | |||
| var queryTime: Long = 0 | |||
| //词典联想结果判断其是否在历史记录中,如果在历史记录中,则进行时间重排序 | |||
| //词典联想结果判断其是否在历史记录中,如果在历史记录中,则进行时间重排序, | |||
| //历史记录表中查询,默认是true | |||
| //词典查询时,需要设置为false | |||
| @Ignore | |||
| var inHistory : Boolean = false | |||
| var inHistory : Boolean = true | |||
| } | |||
| @@ -3,7 +3,11 @@ package com.xkl.cdl.data.exam_record | |||
| import androidx.room.Database | |||
| import androidx.room.Room | |||
| import androidx.room.RoomDatabase | |||
| import androidx.room.migration.Migration | |||
| import androidx.sqlite.db.SupportSQLiteDatabase | |||
| import com.suliang.common.util.file.FileUtil | |||
| import com.xkl.cdl.data.bean.DictionaryBean | |||
| import com.xkl.cdl.data.bean.DictionaryItem | |||
| import com.xkl.cdl.module.XKLApplication | |||
| import mqComsumerV1.Struct | |||
| import java.io.File | |||
| @@ -13,7 +17,7 @@ import java.io.File | |||
| * create 2022/7/7 16:50 | |||
| * Describe: | |||
| */ | |||
| @Database(entities = [Exam::class,ExamItem::class],version = 1,exportSchema = true) | |||
| @Database(entities = [Exam::class,ExamItem::class,DictionaryItem::class], version = 2, exportSchema = true) | |||
| abstract class AppDatabase : RoomDatabase() { | |||
| abstract fun examDao() : ExamDao | |||
| abstract fun examItemDao(): ExamItemDao | |||
| @@ -23,8 +27,18 @@ abstract class AppDatabase : RoomDatabase() { | |||
| companion object{ | |||
| private var DATABASE_NAME :String = "${FileUtil.getSaveDirPath("db")}${File.separator}app.db" | |||
| val MIGRATION_1_2 = object :Migration(1,2){ | |||
| override fun migrate(database : SupportSQLiteDatabase) { | |||
| //版本升级策略,增加一个dictionary表 | |||
| database.execSQL("CREATE TABLE IF NOT EXISTS `dic_history` (`courseId` INTEGER NOT NULL, `queryTime` INTEGER NOT NULL, `id` INTEGER NOT NULL, `word` TEXT NOT NULL, `form` INTEGER NOT NULL, `pre_index` TEXT, `phonetic_uk` TEXT, `phonectic_us` TEXT, `phonectic_cn` TEXT, `basic_explaination` TEXT, `all_explaination` TEXT, `phrase` TEXT, `example` TEXT, `reference` TEXT, PRIMARY KEY(`word`))") | |||
| } | |||
| } | |||
| val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { | |||
| Room.databaseBuilder(XKLApplication.instance(), AppDatabase::class.java, DATABASE_NAME).build() | |||
| Room.databaseBuilder(XKLApplication.instance(), AppDatabase::class.java, DATABASE_NAME) | |||
| .addMigrations(MIGRATION_1_2).build() | |||
| } | |||
| @JvmStatic | |||
| @@ -1,5 +1,6 @@ | |||
| package com.xkl.cdl.data.manager | |||
| import com.suliang.common.AppConfig | |||
| import com.suliang.common.util.file.FileUtil | |||
| import com.xkl.cdl.data.manager.db.DbControlBase | |||
| import java.io.File | |||
| @@ -79,7 +80,20 @@ class FilePathManager { | |||
| fun getMp4File(subjectId : Int,coursePackId : Long,courseId : Long, fileName:String):File{ | |||
| return File(getMp4RootPath(), "$subjectId/$coursePackId/$courseId/$fileName") | |||
| } | |||
| /** 存储音频地址的父文件 */ | |||
| fun getVoiceParent() : File { | |||
| return FileUtil.getSaveDirFile(AppConfig.VOICE) | |||
| } | |||
| /** 词典存储音频地址的父文件 */ | |||
| fun getDicVoiceParent() : File { | |||
| return File(getVoiceParent(),"dic") | |||
| } | |||
| /** vocabulary存储音频地址的父文件 */ | |||
| fun getVocVoiceParent() : File { | |||
| return File(getVoiceParent(),"voc") | |||
| } | |||
| } | |||
| } | |||
| @@ -1,13 +1,17 @@ | |||
| package com.xkl.cdl.data.manager.db | |||
| import android.widget.Toast | |||
| import com.google.common.base.Joiner | |||
| import androidx.core.database.getBlobOrNull | |||
| import com.suliang.common.AppConfig | |||
| import com.suliang.common.util.AppGlobals | |||
| import com.suliang.common.util.LogUtil | |||
| import com.suliang.common.util.file.FileUtil | |||
| import com.suliang.common.util.thread.AppExecutors | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.bean.DictionaryItem | |||
| import com.xkl.cdl.data.manager.FilePathManager | |||
| import net.sqlcipher.database.SQLiteDatabase | |||
| import java.io.File | |||
| import java.lang.StringBuilder | |||
| import java.util.* | |||
| @@ -20,7 +24,7 @@ object DictionaryManager { | |||
| private var mDataBase : SQLiteDatabase? = null | |||
| private var pwd : String = "XKL_XECD_DICT_DATA_KEY" | |||
| private const val DATA_DICT = "dict" | |||
| private const val DATA_DICT = "dict" | |||
| private const val MAX_COUNT = 50 | |||
| private fun open() { | |||
| @@ -57,18 +61,18 @@ object DictionaryManager { | |||
| val titleMap : MutableMap<String, Int> = mutableMapOf() | |||
| var sql = "" | |||
| //1: wor | |||
| sql = "SELECT word FROM $DATA_DICT WHERE word = $keyWord" | |||
| sql = "SELECT word FROM $DATA_DICT WHERE word = '$keyWord'" | |||
| query(sql, titleMap) | |||
| //2: wor% %wor% | |||
| sql = "SELECT word FROM $DATA_DICT WHERE word LIKE $keyWord || '%' LIMIT $MAX_COUNT" //完全匹配搜索:以关键字开头的搜索 | |||
| sql = "SELECT word FROM $DATA_DICT WHERE word LIKE '$keyWord' || '%' LIMIT $MAX_COUNT" //完全匹配搜索:以关键字开头的搜索 | |||
| query(sql, titleMap) | |||
| if (titleMap.size < MAX_COUNT) { | |||
| sql = "SELECT word FROM $DATA_DICT WHERE word LIKE '%' || $keyWord || '%' LIMIT $MAX_COUNT" //查找任意位置包含keyword的值 | |||
| sql = "SELECT word FROM $DATA_DICT WHERE word LIKE '%' || '$keyWord' || '%' LIMIT $MAX_COUNT" //查找任意位置包含keyword的值 | |||
| query(sql, titleMap) | |||
| if (titleMap.size < MAX_COUNT) { | |||
| //3:% w % o % r % | |||
| var newKeyWord : String = insertPercent(keyWord) | |||
| val length = keyWord.length | |||
| val length = newKeyWord.length | |||
| for (i in 0 until length - 1) { | |||
| val max : Int = MAX_COUNT - titleMap.size | |||
| newKeyWord = substringEnd(newKeyWord) | |||
| @@ -100,6 +104,7 @@ object DictionaryManager { | |||
| * @param hashMap | |||
| */ | |||
| private fun query(sql : String, hashMap : MutableMap<String, Int>) { | |||
| LogUtil.e(sql) | |||
| mDataBase?.rawQuery(sql, null)?.let { | |||
| while (it.moveToNext()) { | |||
| if (hashMap.size == MAX_COUNT) break //数量15则打断 | |||
| @@ -173,6 +178,7 @@ object DictionaryManager { | |||
| example = it.getString(13) | |||
| reference = it.getString(14) | |||
| // setVersion(it.getInt(15)) //version版本 | |||
| inHistory = false | |||
| } | |||
| } | |||
| it.close() | |||
| @@ -187,9 +193,10 @@ object DictionaryManager { | |||
| * @param courseId 课程查询的courseId | |||
| * @return | |||
| */ | |||
| fun querys(words : List<String>,courseId : Long = 0 ) : List<DictionaryItem> { | |||
| fun querys(words : String ,courseId : Long = 0 ) : MutableList<DictionaryItem> { | |||
| val list : MutableList<DictionaryItem> = ArrayList<DictionaryItem>() | |||
| val sql = "SELECT * FROM " + DATA_DICT + " WHERE word IN (" + Joiner.on(",").join(words) + ")" | |||
| // val sql = "SELECT * FROM " + DATA_DICT + " WHERE word IN (" + Joiner.on(",").join(words) + ")" | |||
| val sql = "SELECT * FROM $DATA_DICT WHERE word IN ( $words)" | |||
| open() | |||
| mDataBase?.rawQuery(sql, null)?.let { | |||
| while (it.moveToNext()) { | |||
| @@ -210,6 +217,7 @@ object DictionaryManager { | |||
| phrase = it.getString(12) | |||
| example = it.getString(13) | |||
| reference = it.getString(14) | |||
| inHistory = false | |||
| } | |||
| list.add(mDictionaryItem) | |||
| } | |||
| @@ -219,6 +227,49 @@ object DictionaryManager { | |||
| //去重 排序 | |||
| return list.distinctBy { it.word }.sortedBy { | |||
| words.indexOf(it.word) | |||
| }.toMutableList() | |||
| } | |||
| /** | |||
| * 查询发音音频,并写入文件,返回地址 | |||
| * 查询到内容,直接保存为对应发音文件,如果有一个为空,则保存为另一个,如果都为空,则都不保存 | |||
| * @param id Long | |||
| * @param soundWay 发音方式 | |||
| * @return 对应发音的路劲 | |||
| */ | |||
| fun queryAudio(id:Long, soundWay : Int): String?{ | |||
| open() | |||
| val sql = "SELECT audio_uk,audio_us FROM $DATA_DICT WHERE id = $id" | |||
| var result: String? = null | |||
| mDataBase?.rawQuery(sql,null)?.let { | |||
| while (it.moveToNext()){ | |||
| val audioUk = it.getBlobOrNull(0) | |||
| val audioUs = it.getBlobOrNull(0) | |||
| val parentFile = FilePathManager.getDicVoiceParent() | |||
| audioUk?.let { | |||
| val writeFile = File(parentFile,"${id}_${AppConstants.SOUND_TYPE_UK}") | |||
| FileUtil.writeBytesToFile(writeFile,it) | |||
| }?:audioUs?.let { | |||
| val writeFile = File(parentFile,"${id}_${AppConstants.SOUND_TYPE_UK}") | |||
| FileUtil.writeBytesToFile(writeFile,it) | |||
| } | |||
| audioUs?.let { | |||
| val writeFile = File(parentFile,"${id}_${AppConstants.SOUND_TYPE_US}") | |||
| FileUtil.writeBytesToFile(writeFile,it) | |||
| }?:audioUk?.let { | |||
| val writeFile = File(parentFile,"${id}_${AppConstants.SOUND_TYPE_US}") | |||
| FileUtil.writeBytesToFile(writeFile,it) | |||
| } | |||
| if (audioUk != null || audioUs != null){ | |||
| result = File(parentFile,"${id}_$soundWay").path | |||
| } | |||
| } | |||
| it.close() | |||
| } | |||
| return result | |||
| } | |||
| } | |||
| @@ -88,7 +88,7 @@ object VocabularyManager { | |||
| it.close() | |||
| } | |||
| //不为空,写入本身,如果为空,用另外的发音方式写入 | |||
| val parentPath = FileUtil.getSaveDirPath(AppConfig.VOICE) | |||
| val parentPath = FilePathManager.getVocVoiceParent() | |||
| val audio_us_file_path = audio_us?.let { | |||
| val audioFileNameUS = "${wordId}_${AppConstants.SOUND_TYPE_US}" | |||
| val file = File(parentPath, audioFileNameUS) | |||
| @@ -1,13 +1,16 @@ | |||
| package com.xkl.cdl.data.repository | |||
| import android.net.IpPrefix | |||
| import android.util.LruCache | |||
| import androidx.lifecycle.MutableLiveData | |||
| import com.suliang.common.AppConfig | |||
| import com.suliang.common.extension.diskIo2Main | |||
| import com.suliang.common.util.file.FileUtil | |||
| import com.xkl.cdl.data.AppConstants | |||
| import com.xkl.cdl.data.manager.FilePathManager | |||
| import com.xkl.cdl.data.manager.db.DBCourseManager | |||
| import com.xkl.cdl.data.manager.db.DbControlBase | |||
| import com.xkl.cdl.data.manager.db.DictionaryManager | |||
| import com.xkl.cdl.data.manager.db.VocabularyManager | |||
| import io.reactivex.rxjava3.core.Observable | |||
| import java.io.File | |||
| @@ -42,99 +45,126 @@ object AudioCache { | |||
| * 1缓存 2文件 3数据库 | |||
| * @return String 文件路径 | |||
| */ | |||
| fun get(dbControlBase : DbControlBase, wordId : Long, soundWay : Int) { | |||
| fun getLearnVoice(dbControlBase : DbControlBase, wordId : Long, soundWay : Int) { | |||
| //父文件路劲 | |||
| val parentPath = FileUtil.getSaveDirPath(AppConfig.VOICE) | |||
| //先查对应的文件路径 : 项目id_课程包id_课程id_单词id_发音方式 | |||
| val defaultKey = "${dbControlBase.subjectId}_${dbControlBase.coursePackId}_${dbControlBase.courseId}_${wordId}_$soundWay" | |||
| val parentPath = FilePathManager.getVoiceParent() | |||
| //先查对应的文件路径 : 项目id_课程包id_课程id_单词id 发音方式在内部拼接 | |||
| val fileName = "${dbControlBase.subjectId}_${dbControlBase.coursePackId}_${dbControlBase.courseId}_${wordId}" | |||
| getVoicePath(parentPath,fileName,"",soundWay) {DBCourseManager.queryAudio(dbControlBase, wordId, soundWay) } | |||
| } | |||
| /** | |||
| * 获取词汇量测试发音音频 | |||
| * @param wordId Long | |||
| * @param soundWay Int | |||
| */ | |||
| fun getVocabularyVoice(wordId : Long, soundWay : Int){ | |||
| //父文件路劲 | |||
| val parentPath = FilePathManager.getVocVoiceParent() | |||
| getVoicePath(parentPath,"$wordId","v",soundWay) { VocabularyManager.queryAudio(wordId, soundWay) } | |||
| } | |||
| /** | |||
| * 获取词典音频 | |||
| * @param wordId Long | |||
| * @param soundWay Int | |||
| */ | |||
| fun getDictionaryVoice(wordId : Long,soundWay : Int){ | |||
| //父文件路劲 | |||
| val parentPath = FilePathManager.getDicVoiceParent() | |||
| getVoicePath(parentPath,"$wordId","d",soundWay) { DictionaryManager.queryAudio(wordId, soundWay) } | |||
| } | |||
| /** | |||
| * 获取对应发音文件,使用audioLiveData发送回去 | |||
| * 查询缓存 -> 查询文件 -> 查询数据库 | |||
| * @param parentPath File 父文件 | |||
| * @param fileName String 文件名称,没有加发音格式,需要评价发音格式,用于存key的时候,需要添加前缀 | |||
| * @param prefix String 存key时用的前缀,课程、词汇等的时候,为空, 词汇量测试的时候为 "v" , 词典的时候为"d" | |||
| * @param soundWay Int 发音方式,拼接在发音文件后面 | |||
| * @param {} -> String | |||
| */ | |||
| private fun getVoicePath(parentPath:File,fileName:String, prefix : String, soundWay : Int, queryMethod : ()-> String?){ | |||
| val defaultKey = "${prefix}_${fileName}_${soundWay}" | |||
| Observable.fromCallable { | |||
| //不为空,直接取值 | |||
| if (lruCache.get(defaultKey) == null){ | |||
| //为空 查文件 | |||
| val file = File(parentPath, defaultKey) | |||
| when { | |||
| file.exists() -> lruCache.put(defaultKey, file.path) //文件存在,则表示以前进行过数据库查询,且进行了保存文件,英美发音本地都有文件 | |||
| else -> { //文件不存在,则查询数据库进行查询,返回的为保存的文件地址 | |||
| val filePath = DBCourseManager.queryAudio(dbControlBase, wordId, soundWay) | |||
| filePath?.also { | |||
| when(soundWay){ | |||
| AppConstants.SOUND_TYPE_UK -> { | |||
| lruCache.put(defaultKey, it) //保存路径 | |||
| val audioFileNameUS = "${dbControlBase.subjectId}_${dbControlBase.coursePackId}_${dbControlBase.courseId}_${wordId}_${AppConstants.SOUND_TYPE_US}" | |||
| lruCache.put(audioFileNameUS, File(parentPath,audioFileNameUS).path) //保存路径 | |||
| } | |||
| AppConstants.SOUND_TYPE_US -> { | |||
| lruCache.put(defaultKey, it) //保存路径 | |||
| val audioFileNameUS = "${dbControlBase.subjectId}_${dbControlBase.coursePackId}_${dbControlBase.courseId}_${wordId}_${AppConstants.SOUND_TYPE_UK}" | |||
| lruCache.put(audioFileNameUS, File(parentPath,audioFileNameUS).path) //保存路径 | |||
| } | |||
| else -> lruCache.put(defaultKey, it) //保存路径 | |||
| return@fromCallable lruCache[defaultKey]?:let { | |||
| val file = File(parentPath,"${fileName}_$soundWay") //存储的文件 | |||
| if (file.exists()) { | |||
| lruCache.put(defaultKey,file.path) | |||
| }else{ | |||
| queryMethod.invoke()?.let { | |||
| when(soundWay){ | |||
| AppConstants.SOUND_TYPE_US -> { | |||
| val audioUkKey = "${prefix}_${fileName}_${AppConstants.SOUND_TYPE_UK}" | |||
| val audioUk = File(parentPath,"${fileName}_${AppConstants.SOUND_TYPE_UK}").path | |||
| lruCache.put(audioUkKey,audioUk) | |||
| } | |||
| AppConstants.SOUND_TYPE_UK -> { | |||
| val audioUsKey = "${prefix}_${fileName}_${AppConstants.SOUND_TYPE_US}" | |||
| val audioUs = File(parentPath,"${fileName}_${AppConstants.SOUND_TYPE_US}").path | |||
| lruCache.put(audioUsKey,audioUs) | |||
| } | |||
| } //如果查询返回路径为空,则说明,没有发音内容 | |||
| } | |||
| lruCache.put(defaultKey,it) | |||
| } | |||
| } | |||
| lruCache[defaultKey]?:"" | |||
| } | |||
| return@fromCallable lruCache.get(defaultKey) ?: "" | |||
| }.compose(diskIo2Main()).subscribe { | |||
| if (this::audioLiveData.isInitialized) | |||
| audioLiveData.value = it | |||
| audioLiveData.value = it | |||
| } | |||
| } | |||
| /** | |||
| * 口语总测获取发音文件 | |||
| * @param fileName String 发音文件名称 | |||
| * @return String 发音文件路径 | |||
| */ | |||
| fun get(fileName : String):String { | |||
| fun getSpokenVoice(fileName : String):String { | |||
| val parentPath = FileUtil.getSaveDirPath(AppConfig.VOICE) | |||
| if (lruCache[fileName] == null){ | |||
| lruCache.put(fileName,File(parentPath,fileName).path) | |||
| } | |||
| return lruCache[fileName] | |||
| } | |||
| fun getVocabulary(wordId : Long,soundWay : Int){ | |||
| //父文件路劲 | |||
| val parentPath = FileUtil.getSaveDirPath(AppConfig.VOICE) | |||
| //先查对应的文件路径 : 项目id_课程包id_课程id_单词id_发音方式 | |||
| val defaultKey = "${wordId}_$soundWay" | |||
| Observable.fromCallable { | |||
| //不为空,直接取值 | |||
| if (lruCache.get(defaultKey) == null){ | |||
| //为空 查文件 | |||
| val file = File(parentPath, defaultKey) | |||
| when { | |||
| file.exists() -> lruCache.put(defaultKey, file.path) //文件存在,则表示以前进行过数据库查询,且进行了保存文件,英美发音本地都有文件 | |||
| else -> { //文件不存在,则查询数据库进行查询,返回的为保存的文件地址 | |||
| val filePath = VocabularyManager.queryAudio(wordId, soundWay) | |||
| filePath?.also { | |||
| when(soundWay){ | |||
| AppConstants.SOUND_TYPE_UK -> { | |||
| lruCache.put(defaultKey, it) //保存路径 | |||
| val audioFileNameUS = "${wordId}_${AppConstants.SOUND_TYPE_US}" | |||
| lruCache.put(audioFileNameUS, File(parentPath,audioFileNameUS).path) //保存路径 | |||
| } | |||
| AppConstants.SOUND_TYPE_US -> { | |||
| lruCache.put(defaultKey, it) //保存路径 | |||
| val audioFileNameUS = "${wordId}_${AppConstants.SOUND_TYPE_UK}" | |||
| lruCache.put(audioFileNameUS, File(parentPath,audioFileNameUS).path) //保存路径 | |||
| } | |||
| else -> lruCache.put(defaultKey, it) //保存路径 | |||
| /* | |||
| Observable.fromCallable { | |||
| //不为空,直接取值 | |||
| if (lruCache.get(defaultKey) == null){ | |||
| //为空 查文件 | |||
| val file = File(parentPath, defaultKey) | |||
| when { | |||
| file.exists() -> lruCache.put(defaultKey, file.path) //文件存在,则表示以前进行过数据库查询,且进行了保存文件,英美发音本地都有文件 | |||
| else -> { //文件不存在,则查询数据库进行查询,返回的为保存的文件地址 | |||
| val filePath = VocabularyManager.queryAudio(wordId, soundWay) | |||
| filePath?.also { | |||
| when(soundWay){ | |||
| AppConstants.SOUND_TYPE_UK -> { | |||
| lruCache.put(defaultKey, it) //保存路径 | |||
| val audioFileNameUS = "${wordId}_${AppConstants.SOUND_TYPE_US}" | |||
| lruCache.put(audioFileNameUS, File(parentPath,audioFileNameUS).path) //保存路径 | |||
| } | |||
| } //如果查询返回路径为空,则说明,没有发音内容 | |||
| } | |||
| AppConstants.SOUND_TYPE_US -> { | |||
| lruCache.put(defaultKey, it) //保存路径 | |||
| val audioFileNameUS = "${wordId}_${AppConstants.SOUND_TYPE_UK}" | |||
| lruCache.put(audioFileNameUS, File(parentPath,audioFileNameUS).path) //保存路径 | |||
| } | |||
| else -> lruCache.put(defaultKey, it) //保存路径 | |||
| } | |||
| } //如果查询返回路径为空,则说明,没有发音内容 | |||
| } | |||
| } | |||
| return@fromCallable lruCache.get(defaultKey) ?: "" | |||
| }.compose(diskIo2Main()).subscribe { | |||
| if (this::audioLiveData.isInitialized) | |||
| audioLiveData.value = it | |||
| } | |||
| return@fromCallable lruCache.get(defaultKey) ?: "" | |||
| }.compose(diskIo2Main()).subscribe { | |||
| if (this::audioLiveData.isInitialized) | |||
| audioLiveData.value = it | |||
| }*/ | |||
| } | |||
| } | |||
| @@ -536,11 +536,11 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
| if (view is LottieAnimationView && view.visibility == View.VISIBLE) currentPlayView = view | |||
| when{ | |||
| //词汇量测试 | |||
| vm.intentData.examType == AppConstants.TEST_TYPE_NORMAL -> AudioCache.getVocabulary(it.word_id,vm.defaultSoundWay) | |||
| vm.intentData.examType == AppConstants.TEST_TYPE_NORMAL -> AudioCache.getVocabularyVoice(it.word_id, vm.defaultSoundWay) | |||
| //口语 | |||
| isSpokenTotalTest() -> AudioCache.get(view.tag as String) | |||
| isSpokenTotalTest() -> AudioCache.getSpokenVoice(view.tag as String) | |||
| //测试 | |||
| else -> AudioCache.get(vm.dbBaseControl, it.word_id, vm.defaultSoundWay) | |||
| else -> AudioCache.getLearnVoice(vm.dbBaseControl, it.word_id, vm.defaultSoundWay) | |||
| } | |||
| } | |||
| } | |||
| @@ -2,16 +2,13 @@ package com.xkl.cdl.module.learn | |||
| import android.annotation.SuppressLint | |||
| import android.graphics.Color | |||
| import android.opengl.Visibility | |||
| import android.os.Bundle | |||
| import android.text.SpannableStringBuilder | |||
| import android.view.MotionEvent | |||
| import android.view.View | |||
| import android.view.ViewGroup | |||
| import android.widget.TextView | |||
| import androidx.constraintlayout.widget.ConstraintLayout | |||
| import androidx.core.content.ContextCompat | |||
| import androidx.fragment.app.FragmentManager | |||
| import androidx.lifecycle.ViewModelProvider | |||
| import androidx.recyclerview.widget.GridLayoutManager | |||
| import androidx.recyclerview.widget.LinearLayoutManager | |||
| @@ -767,7 +764,7 @@ class LearnWordActivity : BaseActivityVM<ActivityLearnWordBinding, LearnWordView | |||
| 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) | |||
| AudioCache.getLearnVoice(vm.dbControlBase, valueWord.wordId, vm.defaultSoundWay) | |||
| } | |||
| } | |||
| } | |||
| @@ -1,10 +1,8 @@ | |||
| package com.xkl.cdl.module.m_memo | |||
| import android.annotation.SuppressLint | |||
| import android.app.TimePickerDialog | |||
| import android.os.Bundle | |||
| import android.view.View | |||
| import android.widget.TimePicker | |||
| import androidx.lifecycle.ViewModelProvider | |||
| import androidx.recyclerview.widget.LinearLayoutManager | |||
| import com.haibin.calendarview.Calendar | |||
| @@ -28,7 +26,6 @@ import com.xkl.cdl.data.event.LearnEventData | |||
| import com.xkl.cdl.data.repository.AudioCache | |||
| import com.xkl.cdl.databinding.ActivityMemoListDetailBinding | |||
| import com.xkl.cdl.module.learn.LearnWordActivity | |||
| import com.xkl.cdl.module.m_center_learn.coursechildren.CourseMainFragment | |||
| import java.util.* | |||
| import java.util.Calendar.* | |||
| @@ -76,7 +73,7 @@ class MemoListDetailActivity : BaseActivityVM<ActivityMemoListDetailBinding, Mem | |||
| needShowEmptyView = true | |||
| //处理发音事件 | |||
| soundListener = { wordId, soundWay -> | |||
| AudioCache.get(vm.dbControlBase, wordId, soundWay) | |||
| AudioCache.getLearnVoice(vm.dbControlBase, wordId, soundWay) | |||
| } | |||
| } | |||
| } | |||
| @@ -5,18 +5,27 @@ import android.content.Context | |||
| import android.content.Intent | |||
| import android.os.Bundle | |||
| import android.view.View | |||
| import androidx.core.widget.addTextChangedListener | |||
| import androidx.lifecycle.ViewModelProvider | |||
| import androidx.recyclerview.widget.LinearLayoutManager | |||
| import com.suliang.common.AppConfig | |||
| import com.suliang.common.base.activity.BaseActivityVM | |||
| import com.suliang.common.extension.click | |||
| import com.suliang.common.util.LogUtil | |||
| import com.suliang.common.util.media.MPManager | |||
| import com.suliang.common.util.os.KeyboardUtil | |||
| import com.xkl.cdl.R | |||
| import com.xkl.cdl.data.repository.AudioCache | |||
| import com.xkl.cdl.databinding.ActivityDictionaryBinding | |||
| import com.xkl.cdl.dialog.CommonDialog | |||
| import com.xkl.cdl.dialog.CommonDialogBean | |||
| import com.xkl.cdl.module.m_service_center.DictionaryViewModel.Companion.STATE_HISTROY | |||
| import com.xkl.cdl.module.m_service_center.DictionaryViewModel.Companion.STATE_SEARCH | |||
| /** | |||
| * author suliang | |||
| * create 2022/8/9 14:53 | |||
| * Describe: 词典 | |||
| * 历史记录: 查询数据库 历史记录点击进入详情并更新记录 | |||
| * 词典搜索 :1、输入联想搜索 2、联想结果点击详情显示(显示结果,更新记录) 3、联想结果中文详情点击英文item进入详情 | |||
| */ | |||
| class DictionaryActivity : BaseActivityVM<ActivityDictionaryBinding, DictionaryViewModel>() { | |||
| companion object { | |||
| @@ -36,24 +45,28 @@ class DictionaryActivity : BaseActivityVM<ActivityDictionaryBinding, DictionaryV | |||
| override fun initActivity(savedInstanceState : Bundle?) { | |||
| vm.courseId = intent.getLongExtra(AppConfig.INTENT_1, 0) | |||
| binding.titleBar.onBackClick = {finish()} | |||
| initHistoryRecyclerView() | |||
| //点击 查看更多 | |||
| binding.tvSeeMore.click { | |||
| when (vm.detailState) { | |||
| STATE_HISTROY -> { | |||
| DictionaryViewModel.STATE_HISTROY -> { | |||
| //显示页面加1, 列表更新 | |||
| vm.histroyShowPage++ | |||
| vm.adapter.notifyDataSetChanged() | |||
| //根据数量判断,是否还要显示 | |||
| binding.tvSeeMore.visibility = if (vm.historyList.size > vm.histroyShowPage * vm.historyPageSize) View.VISIBLE else View.GONE | |||
| binding.tvSeeMore.visibility = if (vm.historyList.size > vm.histroyShowPage * vm.pageShowCount) View.VISIBLE else View.GONE | |||
| binding.rv.smoothScrollToPosition(vm.histroyShowPage * vm.pageShowCount - 1) | |||
| } | |||
| STATE_SEARCH -> { | |||
| DictionaryViewModel.STATE_SEARCH -> { | |||
| //显示页面加1, 列表更新 | |||
| vm.searchShowPage++ | |||
| vm.adapter.notifyDataSetChanged() | |||
| //根据数量判断,是否还要显示 | |||
| binding.tvSeeMore.visibility = if (vm.searchDetailList.size > vm.searchShowPage * vm.historyPageSize) View.VISIBLE else View.GONE | |||
| binding.tvSeeMore.visibility = if (vm.searchAssociateList.size > vm.searchShowPage * vm.pageShowCount) View.VISIBLE else View.GONE | |||
| binding.rv.smoothScrollToPosition(vm.searchShowPage * vm.pageShowCount - 1) | |||
| } | |||
| } | |||
| } | |||
| @@ -70,96 +83,116 @@ class DictionaryActivity : BaseActivityVM<ActivityDictionaryBinding, DictionaryV | |||
| } | |||
| }.show(supportFragmentManager, javaClass.name) | |||
| } | |||
| } | |||
| private fun initHistoryRecyclerView() { | |||
| binding.rv.apply { | |||
| layoutManager = LinearLayoutManager(this@DictionaryActivity, LinearLayoutManager.VERTICAL, false) | |||
| adapter = vm.adapter | |||
| } | |||
| } | |||
| @SuppressLint("NotifyDataSetChanged") | |||
| override fun loadData() { | |||
| //更新历史记录适配器 | |||
| vm.updateHistoryAdapterLiveData.observe(this) { | |||
| //监听EditText变化 | |||
| // 1 点清空或没有内容时 显示历史记录 | |||
| // 2 关键字搜索 显示搜索记录 | |||
| // 3 点击历史记录或者点击搜索记录,更新显示详情 | |||
| // 4 单独定义一个值,定义为点击了历史记录或搜索记录 | |||
| binding.etSearch.addTextChangedListener{ | |||
| vm.handler.removeCallbacksAndMessages(null) | |||
| when { | |||
| //历史记录 | |||
| !it -> vm.adapter.apply { | |||
| //如果非历史列表记录,则更新列表布局的状态 | |||
| if (vm.detailState != STATE_HISTROY) { | |||
| vm.detailState = STATE_HISTROY | |||
| binding.apply { | |||
| tvHistoryTitle.visibility = View.VISIBLE | |||
| ivDelete.visibility = View.VISIBLE | |||
| } | |||
| } | |||
| binding.layoutDetail.visibility = if (vm.historyList.isEmpty()) View.GONE else View.VISIBLE | |||
| binding.tvSeeMore.visibility = if (vm.historyList.size > vm.histroyShowPage * vm.historyPageSize) View.VISIBLE else View.GONE | |||
| //更新值 | |||
| needShowEmptyView = false | |||
| setData(vm.historyList) | |||
| it.isNullOrEmpty() -> { //内容为空,显示为历史记录 | |||
| LogUtil.e("EditText输入数据为空: 空数据,显示历史记录") | |||
| vm.detailState = DictionaryViewModel.STATE_HISTROY | |||
| vm.updateHistoryAdapterLiveData.value = vm.detailState | |||
| } | |||
| //搜索结果 | |||
| else -> vm.adapter.apply { | |||
| if (vm.detailState != STATE_SEARCH) { | |||
| vm.detailState = STATE_SEARCH | |||
| binding.apply { | |||
| tvHistoryTitle.visibility = View.GONE | |||
| ivDelete.visibility = View.GONE | |||
| else -> { //不为空 | |||
| when(vm.detailState){ | |||
| DictionaryViewModel.STATE_DETAIL -> { //点击显示详情了, 更新详情显示 | |||
| LogUtil.e("EditText输入数据 : 点击item显示详情") | |||
| KeyboardUtil.hideKeyboard(binding.etSearch) //关闭键盘 | |||
| vm.updateHistoryAdapterLiveData.value = vm.detailState | |||
| } | |||
| else -> { //关键字搜索 | |||
| LogUtil.e("EditText输入数据 : 进行关键字搜索") | |||
| vm.detailState = DictionaryViewModel.STATE_SEARCH | |||
| vm.searchKeyWord(it.toString()) | |||
| } | |||
| } | |||
| binding.layoutDetail.visibility = View.VISIBLE | |||
| binding.tvSeeMore.visibility = if (vm.searchDetailList.size > vm.searchShowPage * vm.historyPageSize) View.VISIBLE else View.GONE | |||
| needShowEmptyView = vm.searchDetailList.size == 0 | |||
| setData(vm.searchDetailList) | |||
| } | |||
| } | |||
| } | |||
| /**搜索监听*/ | |||
| vm.etSearchLiveData.observe(this) { | |||
| when { | |||
| //直接历史记录更新 | |||
| it.isNullOrEmpty() -> vm.updateHistoryAdapterLiveData.value = false | |||
| //历史记录的详情 | |||
| vm.searchItemDetailAble -> { | |||
| vm.etSearchLiveData.observe(this){ | |||
| binding.etSearch.setText(it) | |||
| binding.etSearch.setSelection(it.length) | |||
| } | |||
| //更新历史记录适配器 | |||
| // true 跟新搜索结果 | |||
| // false 更新列表 | |||
| vm.updateHistoryAdapterLiveData.observe(this) { | |||
| LogUtil.e("Adapter 进行数据更新 $it") | |||
| when(it){ | |||
| DictionaryViewModel.STATE_HISTROY -> { //历史记录 | |||
| //布局显示处理 | |||
| binding.run { | |||
| tvHistoryTitle.visibility = View.VISIBLE | |||
| ivDelete.visibility = View.VISIBLE | |||
| vLine.visibility = View.VISIBLE | |||
| layoutDetail.visibility = if (vm.historyList.isEmpty()) View.GONE else View.VISIBLE | |||
| } | |||
| binding.tvSeeMore.visibility = if (vm.historyList.size > vm.histroyShowPage * vm.pageShowCount) View.VISIBLE else View.GONE | |||
| vm.adapter.run { | |||
| needShowEmptyView = false | |||
| setData(vm.historyList) | |||
| } | |||
| } | |||
| DictionaryViewModel.STATE_SEARCH -> { //关键字搜索记录 | |||
| binding.run { | |||
| vLine.visibility = View.GONE | |||
| tvHistoryTitle.visibility = View.GONE | |||
| ivDelete.visibility = View.GONE | |||
| layoutDetail.visibility = View.VISIBLE | |||
| tvSeeMore.visibility = if (vm.searchAssociateList.size > vm.searchShowPage * vm.pageShowCount) View.VISIBLE else View.GONE | |||
| } | |||
| vm.searchShowPage = 1 | |||
| vm.adapter.run { | |||
| needShowEmptyView = true | |||
| setData(vm.searchAssociateList) | |||
| } | |||
| } | |||
| DictionaryViewModel.STATE_DETAIL -> { //item详情 | |||
| binding.run { | |||
| vLine.visibility = View.GONE | |||
| tvHistoryTitle.visibility = View.GONE | |||
| ivDelete.visibility = View.GONE | |||
| layoutDetail.visibility = View.VISIBLE | |||
| tvSeeMore.visibility = View.GONE | |||
| } | |||
| vm.adapter.run { | |||
| needShowEmptyView = false | |||
| setData(vm.detailItemList) | |||
| } | |||
| } | |||
| //关键字搜索 | |||
| else -> vm.searchKeyWord(it) | |||
| } | |||
| } | |||
| //查询历史记录 | |||
| vm.loadHistory() | |||
| /**发音监听*/ | |||
| AudioCache.initAudioLiveData().observe(this){ | |||
| it?.let { | |||
| MPManager.play(it) | |||
| }?: showToast("未找到发音文件") | |||
| } | |||
| } | |||
| /** | |||
| * 详情中的状态 | |||
| * @param state Int 1历史记录 2搜索详情 | |||
| */ | |||
| private fun initLayoutDetailState(state : Int) { | |||
| when (state) { | |||
| //历史布局 | |||
| STATE_HISTROY -> binding.apply { | |||
| tvHistoryTitle.visibility = View.VISIBLE | |||
| ivDelete.visibility = View.VISIBLE | |||
| } | |||
| //查询详情 | |||
| STATE_SEARCH -> binding.apply { | |||
| tvHistoryTitle.visibility = View.GONE | |||
| ivDelete.visibility = View.GONE | |||
| } | |||
| private fun initHistoryRecyclerView() { | |||
| binding.rv.apply { | |||
| layoutManager = LinearLayoutManager(this@DictionaryActivity, LinearLayoutManager.VERTICAL, false) | |||
| adapter = vm.adapter | |||
| } | |||
| binding.tvSeeMore.visibility = View.GONE | |||
| } | |||
| @SuppressLint("NotifyDataSetChanged") | |||
| override fun loadData() { | |||
| //查询历史记录 | |||
| vm.loadHistory() | |||
| } | |||
| } | |||
| @@ -4,6 +4,7 @@ import android.os.Handler | |||
| import android.os.Looper | |||
| import androidx.lifecycle.MutableLiveData | |||
| import com.suliang.common.base.viewmodel.BaseViewModel | |||
| import com.suliang.common.extension.io2Main | |||
| import com.suliang.common.util.thread.AppExecutors | |||
| import com.xkl.cdl.adapter.DictionaryAdapter | |||
| import com.xkl.cdl.data.bean.DictionaryItem | |||
| @@ -19,43 +20,44 @@ class DictionaryViewModel : BaseViewModel() { | |||
| //记录状态 | |||
| companion object { | |||
| const val STATE_HISTROY = 1 | |||
| const val STATE_SEARCH = 2 | |||
| const val STATE_HISTROY = 1 //历史记录 | |||
| const val STATE_SEARCH = 2 //关键字搜索结果 | |||
| const val STATE_DETAIL = 3 //详情展示 | |||
| } | |||
| //状态 : 默认历史记录 | |||
| var detailState = STATE_HISTROY | |||
| //适配器 | |||
| val adapter = DictionaryAdapter(this) | |||
| //输入框监听 | |||
| //手动改变editText Value | |||
| val etSearchLiveData = MutableLiveData<String>() | |||
| //搜索关键子 | |||
| var keyWord : String? = null | |||
| //状态 : 默认历史记录 | |||
| var detailState = STATE_HISTROY | |||
| //历史记录集合 | |||
| val historyList = mutableListOf<DictionaryItem>() | |||
| //搜索结果 可为关键字搜索结果 | |||
| var searchAssociateList = mutableListOf<DictionaryItem>() | |||
| //详情结果 固定一个item的大小,即显示详情 | |||
| var detailItemList: MutableList<DictionaryItem>? = null | |||
| //更新详情适配器 false:历史记录 true:联想搜索结果 | |||
| val updateHistoryAdapterLiveData = MutableLiveData<Boolean>() | |||
| //更新详情适配器 STATE_HISTROY历史记录 STATE_SEARCH 搜索结果 STATE_DETAIL 详情 | |||
| val updateHistoryAdapterLiveData = MutableLiveData<Int>() | |||
| //一页显示的历史记录个数,最多历史记录显示30个 搜索结果50个 | |||
| val pageShowCount = 15 | |||
| //历史记录,显示的页数 | |||
| var histroyShowPage = 1 | |||
| var searchShowPage = 1 | |||
| //一页显示的历史记录个数,最多历史记录显示30个 | |||
| val historyPageSize = 10 | |||
| //搜索结果,做多匹配显示50个 | |||
| //是否显示搜索单词的详情 true:显示单条记录的详情 false:搜索显示的为搜索结果的列表详情 | |||
| var searchItemDetailAble = false | |||
| //搜索结果 多条为联想结果 单条基本为搜索的详情结果 | |||
| var searchDetailList = mutableListOf<DictionaryItem>() | |||
| val handler : Handler = Handler(Looper.getMainLooper()) | |||
| //记录联想结果中为历史记录的位置 | |||
| val searchListForHistoryPositionMap = mutableMapOf<Int,Int>() | |||
| private val searchListForHistoryPositionMap = mutableMapOf<Int, Int>() | |||
| /** | |||
| @@ -65,17 +67,19 @@ class DictionaryViewModel : BaseViewModel() { | |||
| Observable.fromCallable { | |||
| val history = AppDatabase.instance.dictionaryDao().query(courseId) | |||
| historyList.addAll(history) | |||
| updateHistoryAdapterLiveData.postValue(false) | |||
| }.observeOn(Schedulers.from(AppExecutors.io)).subscribe() | |||
| if (detailState == STATE_HISTROY) | |||
| updateHistoryAdapterLiveData.postValue(detailState) | |||
| }.subscribeOn(Schedulers.from(AppExecutors.io)).subscribe() | |||
| } | |||
| /**清空历史记录*/ | |||
| fun clearHistory() { | |||
| Observable.fromCallable { | |||
| historyList.clear() | |||
| updateHistoryAdapterLiveData.postValue(false) | |||
| if (detailState == STATE_HISTROY) | |||
| updateHistoryAdapterLiveData.postValue(detailState) | |||
| AppDatabase.instance.dictionaryDao().clear(courseId) | |||
| }.observeOn(Schedulers.from(AppExecutors.io)).subscribe() | |||
| }.subscribeOn(Schedulers.from(AppExecutors.io)).subscribe() | |||
| } | |||
| /** | |||
| @@ -83,17 +87,17 @@ class DictionaryViewModel : BaseViewModel() { | |||
| * @param keyWord String | |||
| */ | |||
| fun searchKeyWord(keyWord : String) { | |||
| handler.removeCallbacksAndMessages(null) | |||
| this.keyWord = keyWord | |||
| handler.postDelayed({ | |||
| Observable.fromCallable { | |||
| //清空位置对应信息 | |||
| searchListForHistoryPositionMap.clear() | |||
| //联想查询结果 | |||
| searchDetailList = DictionaryManager.queryKeyWordLikeMatchSimple(keyWord) | |||
| searchAssociateList = DictionaryManager.queryKeyWordLikeMatchSimple(keyWord) | |||
| //搜索记录中获取历史记录位置 需要判断是否有历史记录 | |||
| if (historyList.isNotEmpty() && searchDetailList.isNotEmpty()) { | |||
| if (historyList.isNotEmpty() && searchAssociateList.isNotEmpty()) { | |||
| historyList.forEachIndexed { historyIndex, historyItem -> | |||
| searchDetailList.forEachIndexed { searchIndex, searchItem -> | |||
| searchAssociateList.forEachIndexed { searchIndex, searchItem -> | |||
| if (historyItem.word == searchItem.word) { | |||
| searchItem.inHistory = true | |||
| searchListForHistoryPositionMap.put(searchIndex, historyIndex) | |||
| @@ -101,11 +105,84 @@ class DictionaryViewModel : BaseViewModel() { | |||
| } | |||
| } | |||
| } | |||
| //更新搜索结果 | |||
| updateHistoryAdapterLiveData.postValue(true) | |||
| }.observeOn(Schedulers.from(AppExecutors.io)).subscribe() | |||
| }, 300) | |||
| //更新搜索结果 : 避免搜索结束后,显示状态已改变,发送出去消息后,造成更新错乱 | |||
| if (detailState == STATE_SEARCH) { | |||
| updateHistoryAdapterLiveData.postValue(detailState) | |||
| } | |||
| }.subscribeOn(Schedulers.from(AppExecutors.io)).subscribe() | |||
| }, 200) | |||
| } | |||
| /** | |||
| * 查询中文item下的多个英文item集合 | |||
| * @param it DictionaryItem | |||
| * @return MutableList<DictionaryItem>? | |||
| */ | |||
| fun queryChineseForEnglishList(chineseItem : DictionaryItem) { | |||
| Observable.fromCallable { | |||
| //清空位置对应信息 | |||
| searchListForHistoryPositionMap.clear() | |||
| //精准查询结果 | |||
| return@fromCallable chineseItem.basic_explaination?.let { | |||
| val replace = it.replace(";", "\',\'") | |||
| val querys = DictionaryManager.querys("\'$replace\'") | |||
| //搜索记录中获取历史记录位置 需要判断是否有历史记录 | |||
| if (historyList.isNotEmpty() && querys.isNotEmpty()) { | |||
| historyList.forEachIndexed { historyIndex, historyItem -> | |||
| querys.forEachIndexed { searchIndex, searchItem -> | |||
| if (historyItem.word == searchItem.word) { | |||
| searchItem.inHistory = true | |||
| searchListForHistoryPositionMap.put(searchIndex, historyIndex) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| querys | |||
| } ?: mutableListOf<DictionaryItem>() | |||
| }.compose(io2Main()).subscribe { | |||
| if (detailState == STATE_DETAIL) | |||
| adapter.adapterChinese.setData(it) | |||
| } | |||
| } | |||
| /** | |||
| * 添加历史记录中 | |||
| * @param item DictionaryItem | |||
| * @param position Int 搜索列表中的位置 | |||
| */ | |||
| fun insertAndUpdateHistory(item : DictionaryItem, position : Int) { | |||
| detailState = STATE_DETAIL | |||
| Observable.fromCallable { | |||
| item.queryTime = System.currentTimeMillis() | |||
| //添加到历史记录第一个 | |||
| if (!item.inHistory) { | |||
| item.inHistory = true | |||
| if (historyList.isNotEmpty() && historyList.size >= 30) { | |||
| historyList.removeLast() //移除最后一个 | |||
| } | |||
| } else if(detailState == STATE_SEARCH){ // 搜索结果的item点击,需要移除历史记录中对应的位置 | |||
| searchListForHistoryPositionMap.get(position)?.let { | |||
| historyList.removeAt(it) //移除其在历史记录中的位置 | |||
| } | |||
| }else if (detailState == STATE_HISTROY){ //历史记录的item点击,移除该条历史记录 | |||
| historyList.removeAt(position) | |||
| } | |||
| //添加到历史记录第一个 | |||
| historyList.add(0, item) | |||
| //重新赋值需要用于刷新的集合 | |||
| detailItemList = mutableListOf(item) | |||
| //更改inputEditView的值,重新赋值列表集合 | |||
| etSearchLiveData.postValue(item.word) | |||
| //更新或插入历史搜索记录中 | |||
| AppDatabase.instance.dictionaryDao().insert(item) | |||
| }.subscribeOn(Schedulers.from(AppExecutors.io)).subscribe() | |||
| } | |||
| } | |||
| @@ -54,11 +54,13 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() { | |||
| //复制词典 和 词汇量测试 | |||
| val dictionary = FilePathManager.getDictionaryDbPath() | |||
| if (!dictionary.exists()) { | |||
| FileUtil.copyAsset("dictionary.db", dictionary) | |||
| if (!dictionary.exists() || dictionary.length() != FileUtil.getAssetFileSize("dictionary.db")) { | |||
| LogUtil.e("复制dictionary.db") | |||
| FileUtil.copyAsset("dictionary.db", dictionary) | |||
| } | |||
| val vocabulary = FilePathManager.getVocabularyDbPath() | |||
| if (!vocabulary.exists()) { | |||
| if (!vocabulary.exists() || vocabulary.length() != FileUtil.getAssetFileSize("vocabulary.db")) { | |||
| LogUtil.e("复制vocabulary.db") | |||
| FileUtil.copyAsset("vocabulary.db", vocabulary) | |||
| } | |||
| @@ -1,14 +1,15 @@ | |||
| package com.xkl.cdl.util | |||
| import android.text.Html | |||
| import android.text.Spanned | |||
| import android.text.Spannable | |||
| import android.text.SpannableStringBuilder | |||
| import android.text.style.ForegroundColorSpan | |||
| import android.widget.TextView | |||
| import androidx.annotation.ColorInt | |||
| import androidx.annotation.ColorRes | |||
| import androidx.core.content.ContextCompat | |||
| import com.suliang.common.util.AppGlobals | |||
| import com.suliang.common.util.ColorUtil | |||
| import com.suliang.common.util.os.ScreenUtil | |||
| import java.util.* | |||
| import java.util.regex.Pattern | |||
| /** | |||
| @@ -103,6 +104,6 @@ class ViewUtil { | |||
| m.appendTail(buffer) | |||
| return buffer.toString() | |||
| } | |||
| } | |||
| } | |||
| @@ -4,9 +4,6 @@ | |||
| xmlns:tools="http://schemas.android.com/tools"> | |||
| <data> | |||
| <variable | |||
| name="vm" | |||
| type="com.xkl.cdl.module.m_service_center.DictionaryViewModel" /> | |||
| </data> | |||
| <androidx.constraintlayout.widget.ConstraintLayout | |||
| @@ -58,26 +55,31 @@ | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:background="@drawable/shape_rounder_8_stroke_gray1" | |||
| android:imeOptions="actionDone" | |||
| android:paddingStart="@dimen/global_spacing" | |||
| android:paddingEnd="@dimen/global_spacing" | |||
| android:drawablePadding="4dp" | |||
| android:textSize="@dimen/smallSize" | |||
| android:gravity="center_vertical" | |||
| android:hint="输入你要搜索的内容…" | |||
| android:text="@{vm.etSearchLiveData}"/> | |||
| android:singleLine="true" | |||
| android:hint="输入你要搜索的内容…"/> | |||
| <androidx.constraintlayout.widget.ConstraintLayout | |||
| android:id="@+id/layout_detail" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| android:layout_height="0dp" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/et_search" | |||
| app:layout_constraintBottom_toBottomOf="parent" | |||
| app:layout_constraintVertical_bias="0" | |||
| android:layout_marginTop="12dp" | |||
| android:layout_marginBottom="34dp" | |||
| android:background="@drawable/shape_rounder_8_solid_green1" | |||
| android:backgroundTint="@color/gray_3" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:layout_marginEnd="@dimen/global_spacing"> | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| app:layout_constrainedHeight="true"> | |||
| <View | |||
| android:id="@+id/v_line" | |||
| @@ -115,7 +117,12 @@ | |||
| android:id="@+id/rv" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content" | |||
| app:layout_constraintTop_toBottomOf="@+id/v_line" /> | |||
| app:layout_constraintTop_toBottomOf="@+id/v_line" | |||
| app:layout_constraintBottom_toTopOf="@+id/tv_see_more" | |||
| app:layout_constraintVertical_chainStyle="packed" | |||
| app:layout_constraintVertical_bias="0" | |||
| app:layout_constrainedHeight="true" | |||
| tools:itemCount="10"/> | |||
| <TextView | |||
| android:id="@+id/tv_see_more" | |||
| @@ -126,6 +133,8 @@ | |||
| app:layout_constraintTop_toBottomOf="@+id/rv" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintBottom_toBottomOf="parent" | |||
| app:layout_constraintVertical_bias="0" | |||
| android:text="查看更多" | |||
| android:drawableEnd="@drawable/ic_down" | |||
| android:drawableTint="@color/gray_2" | |||
| @@ -1,47 +1,53 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content" | |||
| <layout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| xmlns:app="http://schemas.android.com/apk/res-auto" | |||
| xmlns:tools="http://schemas.android.com/tools"> | |||
| <TextView | |||
| android:id="@+id/tv_word" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:layout_marginTop="16dp" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:text="jaapjaapjaap" | |||
| android:textColor="@color/main_text_color" | |||
| android:textSize="22dp" | |||
| android:textStyle="bold" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintHorizontal_bias="0.0" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toTopOf="parent" /> | |||
| <TextView | |||
| android:id="@+id/tv_phonetic" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:gravity="center_vertical" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallerSize" | |||
| app:layout_constraintEnd_toEndOf="@+id/tv_word" | |||
| app:layout_constraintStart_toStartOf="@+id/tv_word" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_word" | |||
| tools:text="[gud:bai] " | |||
| app:layout_constraintHorizontal_bias="0" | |||
| android:layout_marginTop="8dp"/> | |||
| <androidx.recyclerview.widget.RecyclerView | |||
| android:id="@+id/recyclerView" | |||
| <data> | |||
| </data> | |||
| <androidx.constraintlayout.widget.ConstraintLayout | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_phonetic" | |||
| /> | |||
| android:layout_height="wrap_content"> | |||
| <TextView | |||
| android:id="@+id/tv_word" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:layout_marginTop="16dp" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:text="jaapjaapjaap" | |||
| android:textColor="@color/main_text_color" | |||
| android:textSize="22dp" | |||
| android:textStyle="bold" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintHorizontal_bias="0.0" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toTopOf="parent" /> | |||
| <TextView | |||
| android:id="@+id/tv_phonetic" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:gravity="center_vertical" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallerSize" | |||
| app:layout_constraintEnd_toEndOf="@+id/tv_word" | |||
| app:layout_constraintStart_toStartOf="@+id/tv_word" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_word" | |||
| tools:text="[gud:bai] " | |||
| app:layout_constraintHorizontal_bias="0" | |||
| android:layout_marginTop="8dp" /> | |||
| <androidx.recyclerview.widget.RecyclerView | |||
| android:id="@+id/recyclerView" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_phonetic" /> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| </layout> | |||
| @@ -1,55 +1,61 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content" | |||
| <layout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| xmlns:tools="http://schemas.android.com/tools" | |||
| xmlns:app="http://schemas.android.com/apk/res-auto" | |||
| android:orientation="horizontal" | |||
| android:gravity="center_vertical"> | |||
| xmlns:app="http://schemas.android.com/apk/res-auto"> | |||
| <TextView | |||
| android:id="@+id/tv_word" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:singleLine="true" | |||
| tools:text="introduction" | |||
| android:textSize="@dimen/normalSize" | |||
| android:textColor="@color/main_text_color" | |||
| android:layout_marginTop="8dp" | |||
| app:layout_constraintTop_toTopOf="parent" | |||
| app:layout_constraintStart_toEndOf="@+id/iv_horuns" | |||
| android:gravity="center_vertical" | |||
| /> | |||
| <ImageView | |||
| android:id="@+id/iv_horuns" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| tools:src="@drawable/ic_horuns" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:layout_marginEnd="8dp" | |||
| app:layout_constraintTop_toTopOf="@+id/tv_word" | |||
| app:layout_constraintBottom_toBottomOf="@+id/tv_word" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| android:paddingRight="4dp"/> | |||
| <TextView | |||
| android:id="@+id/tv_value" | |||
| android:layout_width="wrap_content" | |||
| <data> | |||
| </data> | |||
| <androidx.constraintlayout.widget.ConstraintLayout | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:textSize="@dimen/smallerSize" | |||
| android:textColor="@color/gray_2" | |||
| tools:text="简介" | |||
| app:layout_constrainedWidth="true" | |||
| app:layout_constraintHorizontal_bias="0" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_word" | |||
| app:layout_constraintBottom_toBottomOf="parent" | |||
| android:layout_marginBottom="8dp" | |||
| android:layout_marginTop="4dp"/> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| android:orientation="horizontal" | |||
| android:gravity="center_vertical"> | |||
| <TextView | |||
| android:id="@+id/tv_word" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:singleLine="true" | |||
| tools:text="introduction" | |||
| android:textSize="@dimen/normalSize" | |||
| android:textColor="@color/main_text_color" | |||
| android:layout_marginTop="8dp" | |||
| app:layout_constraintTop_toTopOf="parent" | |||
| app:layout_constraintStart_toEndOf="@+id/iv_horuns" | |||
| android:gravity="center_vertical" /> | |||
| <ImageView | |||
| android:id="@+id/iv_horuns" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:src="@drawable/ic_horuns" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:layout_marginEnd="8dp" | |||
| app:layout_constraintTop_toTopOf="@+id/tv_word" | |||
| app:layout_constraintBottom_toBottomOf="@+id/tv_word" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| android:paddingRight="4dp" /> | |||
| <TextView | |||
| android:id="@+id/tv_value" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:textSize="@dimen/smallerSize" | |||
| android:textColor="@color/gray_2" | |||
| tools:text="简介" | |||
| app:layout_constrainedWidth="true" | |||
| app:layout_constraintHorizontal_bias="0" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_word" | |||
| app:layout_constraintBottom_toBottomOf="parent" | |||
| android:layout_marginBottom="8dp" | |||
| android:layout_marginTop="4dp" /> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| </layout> | |||
| @@ -1,94 +1,101 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content" | |||
| <layout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| xmlns:app="http://schemas.android.com/apk/res-auto" | |||
| xmlns:tools="http://schemas.android.com/tools"> | |||
| <TextView | |||
| android:id="@+id/tv_word" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:layout_marginTop="16dp" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:text="jaapjaapjaap" | |||
| android:textColor="@color/main_text_color" | |||
| android:textSize="22dp" | |||
| android:textStyle="bold" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintHorizontal_bias="0.0" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toTopOf="parent" /> | |||
| <data> | |||
| <com.suliang.common.widget.TagLinearLayout | |||
| android:id="@+id/layout_phonetic" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| app:item_horizontal_space="16dp" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="@+id/tv_word" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_word" | |||
| android:layout_marginTop="8dp" | |||
| android:layout_marginEnd="@dimen/global_spacing"> | |||
| </data> | |||
| <androidx.constraintlayout.widget.ConstraintLayout | |||
| android:layout_width="match_parent" | |||
| android:layout_height="wrap_content"> | |||
| <TextView | |||
| android:id="@+id/tv_phonetic_uk" | |||
| android:layout_width="wrap_content" | |||
| android:id="@+id/tv_word" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| android:drawableEnd="@drawable/ic_horuns" | |||
| android:gravity="center" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallerSize" | |||
| tools:text="英 [gud:bai] " /> | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:layout_marginTop="16dp" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:text="jaapjaapjaap" | |||
| android:textColor="@color/main_text_color" | |||
| android:textSize="22dp" | |||
| android:textStyle="bold" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintHorizontal_bias="0.0" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintTop_toTopOf="parent" /> | |||
| <TextView | |||
| android:id="@+id/tv_phonetic_us" | |||
| android:layout_width="wrap_content" | |||
| <com.suliang.common.widget.TagLinearLayout | |||
| android:id="@+id/layout_phonetic" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| android:drawableEnd="@drawable/ic_horuns" | |||
| android:gravity="center" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallerSize" | |||
| tools:text="美 [gud:bai] " /> | |||
| app:item_horizontal_space="16dp" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="@+id/tv_word" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_word" | |||
| android:layout_marginTop="8dp" | |||
| android:layout_marginEnd="@dimen/global_spacing"> | |||
| <TextView | |||
| android:id="@+id/tv_phonetic_uk" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:drawableEnd="@drawable/ic_horuns" | |||
| android:gravity="center" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallerSize" | |||
| tools:text="英 [gud:bai] " /> | |||
| <TextView | |||
| android:id="@+id/tv_phonetic_us" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:drawableEnd="@drawable/ic_horuns" | |||
| android:gravity="center" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallerSize" | |||
| tools:text="美 [gud:bai] " /> | |||
| </com.suliang.common.widget.TagLinearLayout> | |||
| </com.suliang.common.widget.TagLinearLayout> | |||
| <TextView | |||
| android:id="@+id/tv_explain" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginTop="10dp" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:textColor="@color/main_text_color" | |||
| android:textSize="@dimen/smallSize" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="@+id/tv_word" | |||
| app:layout_constraintTop_toBottomOf="@+id/layout_phonetic" | |||
| app:layout_constraintHorizontal_bias="0" | |||
| android:textStyle="bold" | |||
| tools:text="基本释义"/> | |||
| <TextView | |||
| android:id="@+id/tv_explain" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginTop="10dp" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:textColor="@color/main_text_color" | |||
| android:textSize="@dimen/smallSize" | |||
| app:layout_constraintEnd_toEndOf="parent" | |||
| app:layout_constraintStart_toStartOf="@+id/tv_word" | |||
| app:layout_constraintTop_toBottomOf="@+id/layout_phonetic" | |||
| app:layout_constraintHorizontal_bias="0" | |||
| android:textStyle="bold" | |||
| tools:text="基本释义" /> | |||
| <TextView | |||
| android:id="@+id/tv_expand_explain" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginTop="2dp" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallSize" | |||
| app:layout_constraintEnd_toEndOf="@+id/tv_word" | |||
| app:layout_constraintStart_toStartOf="@+id/tv_word" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_explain" | |||
| tools:text="扩展释义>"/> | |||
| <TextView | |||
| android:id="@+id/tv_expand_explain" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| android:layout_marginTop="2dp" | |||
| android:textColor="@color/gray_2" | |||
| android:textSize="@dimen/smallSize" | |||
| app:layout_constraintEnd_toEndOf="@+id/tv_word" | |||
| app:layout_constraintStart_toStartOf="@+id/tv_word" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_explain" | |||
| tools:text="扩展释义>" /> | |||
| <include | |||
| android:id="@+id/inc_word_detail" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| layout="@layout/inc_word_detail" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_expand_explain" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent"/> | |||
| <include | |||
| android:id="@+id/inc_word_detail" | |||
| android:layout_width="0dp" | |||
| android:layout_height="wrap_content" | |||
| layout="@layout/inc_word_detail" | |||
| app:layout_constraintTop_toBottomOf="@+id/tv_expand_explain" | |||
| app:layout_constraintStart_toStartOf="parent" | |||
| app:layout_constraintEnd_toEndOf="parent" /> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| </androidx.constraintlayout.widget.ConstraintLayout> | |||
| </layout> | |||
| @@ -1,40 +1,46 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="32dp" | |||
| xmlns:tools="http://schemas.android.com/tools" | |||
| android:orientation="horizontal" | |||
| android:gravity="center_vertical"> | |||
| <ImageView | |||
| android:id="@+id/iv" | |||
| android:layout_width="16dp" | |||
| android:layout_height="16dp" | |||
| tools:src="@drawable/ic_clock" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:layout_marginEnd="8dp"/> | |||
| <TextView | |||
| android:id="@+id/tv_word" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:singleLine="true" | |||
| tools:text="jaap" | |||
| android:textSize="@dimen/smallSize" | |||
| android:textColor="@color/main_text_color" | |||
| /> | |||
| <TextView | |||
| android:id="@+id/tv_value" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:singleLine="true" | |||
| android:ellipsize="end" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:textSize="@dimen/smallSize" | |||
| android:textColor="@color/main_text_color" | |||
| tools:text="治疗"/> | |||
| </LinearLayout> | |||
| <layout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| xmlns:tools="http://schemas.android.com/tools"> | |||
| <data> | |||
| </data> | |||
| <LinearLayout | |||
| android:layout_width="match_parent" | |||
| android:layout_height="32dp" | |||
| android:orientation="horizontal" | |||
| android:gravity="center_vertical"> | |||
| <ImageView | |||
| android:id="@+id/iv" | |||
| android:layout_width="16dp" | |||
| android:layout_height="16dp" | |||
| tools:src="@drawable/ic_clock" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:layout_marginEnd="8dp" /> | |||
| <TextView | |||
| android:id="@+id/tv_word" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:singleLine="true" | |||
| tools:text="jaap" | |||
| android:textSize="@dimen/smallSize" | |||
| android:textColor="@color/main_text_color" /> | |||
| <TextView | |||
| android:id="@+id/tv_value" | |||
| android:layout_width="wrap_content" | |||
| android:layout_height="wrap_content" | |||
| android:singleLine="true" | |||
| android:ellipsize="end" | |||
| android:layout_marginEnd="@dimen/global_spacing" | |||
| android:layout_marginStart="@dimen/global_spacing" | |||
| android:textSize="@dimen/smallSize" | |||
| android:textColor="@color/main_text_color" | |||
| tools:text="治疗" /> | |||
| </LinearLayout> | |||
| </layout> | |||
| @@ -43,7 +43,8 @@ object LogUtil { | |||
| if (!LogEnable) return | |||
| val stackTraceElement = Thread.currentThread().stackTrace[5] | |||
| val className = stackTraceElement.className.substringAfterLast(".") | |||
| val out = "==> \n${className}_${stackTraceElement.methodName.substringAfterLast(".")}_${stackTraceElement.lineNumber} \n$message" | |||
| // val out = "==> \n${className}_${stackTraceElement.methodName.substringAfterLast(".")}_${stackTraceElement.lineNumber} \n$message" | |||
| val out = "==> $className \n$message" | |||
| when(logType){ | |||
| "v" -> Log.v(className,out) | |||
| "d" -> Log.d(className,out) | |||
| @@ -1,8 +1,8 @@ | |||
| package com.suliang.common.util | |||
| import android.text.Html | |||
| import android.text.Spanned | |||
| import androidx.annotation.ColorInt | |||
| import android.text.Spannable | |||
| import android.text.SpannableStringBuilder | |||
| import android.text.style.ForegroundColorSpan | |||
| import java.util.regex.Pattern | |||
| /** | |||
| @@ -14,9 +14,25 @@ class StringUtil { | |||
| companion object { | |||
| /** | |||
| * 匹配指定字符串,并对匹配的字符串每个字符都进行匹配,进行高亮 | |||
| * @param word String | |||
| * @param needHighStr String | |||
| * @param color Int | |||
| */ | |||
| fun highString(word : String, needHighStr : String?, color : Int) : SpannableStringBuilder { | |||
| val result = SpannableStringBuilder(word) | |||
| if (!needHighStr.isNullOrEmpty()){ | |||
| val compile = Pattern.compile("[${needHighStr.lowercase()}]") // 匹配needHighStr中的所有匹配字符 | |||
| val matcher = compile.matcher(word.lowercase()) | |||
| while (matcher.find()) { | |||
| val start = matcher.start() //开始位置 | |||
| val end = matcher.end() //结束位置 | |||
| result.setSpan(ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) | |||
| } | |||
| } | |||
| return result | |||
| } | |||
| } | |||
| } | |||
| @@ -4,7 +4,6 @@ import android.os.Environment | |||
| import com.suliang.common.util.AppGlobals | |||
| import com.suliang.common.util.LogUtil | |||
| import java.io.* | |||
| import java.lang.Exception | |||
| import java.text.DecimalFormat | |||
| import java.util.zip.GZIPInputStream | |||
| import java.util.zip.GZIPOutputStream | |||
| @@ -446,6 +445,16 @@ object FileUtil { | |||
| } | |||
| return result | |||
| } | |||
| fun getAssetFileSize(assetName:String):Long{ | |||
| try { | |||
| val open = AppGlobals.application.assets.open(assetName) | |||
| return open.available().toLong() | |||
| }catch (e:Exception){ | |||
| e.printStackTrace() | |||
| } | |||
| return 0 | |||
| } | |||
| /* private fun getFileSize(file: File) : Int{ | |||
| if (file.exists()){ | |||