diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8d175c01..4e0cee42 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,6 +26,7 @@ + + suspend fun retrofitDownLoadFile(@Header("RANGE") start: String? = "0", @Url url: String):Response /** diff --git a/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadManager.kt b/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadManager.kt index cc720498..d38896f0 100644 --- a/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadManager.kt +++ b/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadManager.kt @@ -1,48 +1,126 @@ package com.navinfo.omqs.http.offlinemapdownload import android.content.Context -import android.os.Environment import android.text.TextUtils +import android.util.Log +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer import com.navinfo.omqs.Constant import com.navinfo.omqs.bean.OfflineMapCityBean +import com.navinfo.omqs.http.RetrofitNetworkServiceAPI import dagger.hilt.android.qualifiers.ActivityContext -import java.io.Serializable +import kotlinx.coroutines.cancel import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject /** * 管理离线地图下载 */ -class OfflineMapDownloadManager @Inject constructor(@ActivityContext context: Context) { +class OfflineMapDownloadManager @Inject constructor( + private val netApi: RetrofitNetworkServiceAPI +) { /** * 最多同时下载数量 */ - private val MAX_SCOPE = 5 + private val MAX_SCOPE = 3 /** - * 存储有哪些城市需要下载 + * 存储有哪些城市需要下载的队列 */ private val scopeMap: ConcurrentHashMap by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { ConcurrentHashMap() } - val downloadFolder: String? by lazy { - Constant.MAP_PATH + "/offline/" + /** + * 存储正在下载的城市队列 + */ + private val taskScopeMap: ConcurrentHashMap by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { + ConcurrentHashMap() } /** + * 启动下载任务 + * 请不要直接使用此方法启动下载任务,它是交由[OfflineMapDownloadScope]进行调用 + */ + fun launchScope(scope: OfflineMapDownloadScope) { + if (taskScopeMap.size >= MAX_SCOPE) { + return + } + if (taskScopeMap.contains(scope.cityBean.id)) { + return + } + taskScopeMap[scope.cityBean.id] = scope + scope.launch() + } + + /** + * 启动下一个任务,如果有正在等待中的任务的话 + * 请不要直接使用此方法启动下载任务,它是交由[OfflineMapDownloadScope]进行调用 + * @param previousUrl 上一个下载任务的下载连接 + */ + fun launchNext(previousUrl: String) { + taskScopeMap.remove(previousUrl) + for (entrySet in scopeMap) { + val downloadScope = entrySet.value + if (downloadScope.isWaiting()) { + launchScope(downloadScope) + break + } + } + } + + /** + * 暂停任务 + * 只有等待中的任务和正在下载中的任务才可以进行暂停操作 + */ + fun pause(id: String) { + if (taskScopeMap.containsKey(id)) { + val downloadScope = taskScopeMap[id] + downloadScope?.let { + downloadScope.pause() + } + launchNext(id) + } + + } + + /** + * 将下载任务加入到协程作用域的下载队列里 * 请求一个下载任务[OfflineMapDownloadScope] * 这是创建[OfflineMapDownloadScope]的唯一途径,请不要通过其他方式创建[OfflineMapDownloadScope] */ - fun request(cityBean: OfflineMapCityBean): OfflineMapDownloadScope? { - //没有下载连接的不能下载 - if (TextUtils.isEmpty(cityBean.url)) return null -// if(scopeMap.containsKey()) - var downloadScope = scopeMap[cityBean.id] - if (downloadScope == null) { - scopeMap[cityBean.id] = OfflineMapDownloadScope(cityBean) - } - return downloadScope + fun start(id: String) { + scopeMap[id]?.start() } + + fun cancel(id: String) { + taskScopeMap.remove(id) + scopeMap[id]?.cancelTask() + } + + fun addTask(cityBean: OfflineMapCityBean) { + if (scopeMap.containsKey(cityBean.id)) { + return + } else { + scopeMap[cityBean.id] = OfflineMapDownloadScope(this, netApi, cityBean) + } + } + + + fun observer( + id: String, + lifecycleOwner: LifecycleOwner, + observer: Observer + ) { + if (scopeMap.containsKey(id)) { + val downloadScope = scopeMap[id] + downloadScope?.let { + downloadScope.observer(lifecycleOwner, observer) + } + } + } + + + } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadScope.kt b/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadScope.kt index a51f93d5..c518b504 100644 --- a/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadScope.kt +++ b/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadScope.kt @@ -1,26 +1,255 @@ package com.navinfo.omqs.http.offlinemapdownload +import android.util.Log +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import com.navinfo.omqs.Constant import com.navinfo.omqs.bean.OfflineMapCityBean -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job +import com.navinfo.omqs.http.RetrofitNetworkServiceAPI +import kotlinx.coroutines.* +import java.io.File +import java.io.IOException +import java.io.RandomAccessFile import kotlin.coroutines.EmptyCoroutineContext /** * 代表一个下载任务 * [OfflineMapCityBean.id]将做为下载任务的唯一标识 - * 不要直接在外部直接创建此对象,那样就可能无法统一管理下载任务,请通过[OfflineMapDownloadManager.download]获取此对象 + * 不要直接在外部直接创建此对象,那样就可能无法统一管理下载任务,请通过[OfflineMapDownloadManager.request]获取此对象 * 这是一个协程作用域, * EmptyCoroutineContext 表示一个不包含任何元素的协程上下文,它通常用于创建新的协程上下文,或者作为协程上下文的基础。 */ -class OfflineMapDownloadScope(cityBean: OfflineMapCityBean) : CoroutineScope by CoroutineScope(EmptyCoroutineContext) { +class OfflineMapDownloadScope( + private val downloadManager: OfflineMapDownloadManager, + private val netApi: RetrofitNetworkServiceAPI, + val cityBean: OfflineMapCityBean + +) : + CoroutineScope by CoroutineScope(EmptyCoroutineContext) { /** - * + *下载任务,用来取消的 */ private var downloadJob: Job? = null + /** + * 管理观察者,同时只有一个就行了 + */ + private var observer: Observer? = null + /** * */ private val downloadData = MutableLiveData() + + init { + downloadData.value = cityBean + } + + /** + * 开始任务的下载 + * [OfflineMapCityBean]是在协程中进行创建的,它的创建会优先从数据库和本地文件获取,但这种操作是异步的,详情请看init代码块 + * 我们需要通过观察者观察[OfflineMapCityBean]来得知它是否已经创建完成,只有当他创建完成且不为空(如果创建完成,它一定不为空) + * 才可以交由[OfflineMapDownloadManager]进行下载任务的启动 + * 任务的开始可能并不是立即的,任务会受到[OfflineMapDownloadManager]的管理 + * + * 这段原来代码没看懂:要触发 Observer 得观察的对象[OfflineMapCityBean]发生变化才行,原demo里没找到livedata的变化也触发了onChange,这里根本触发不了 + * + * 找到原因了:是[cityBean]根本没有设置到liveData中,但是还是不用这样了,因为cityBean是一定创建好了的 + */ + //原代码 +// fun start() { +// var observer: Observer? = null +// observer = Observer { cityBean -> +// Log.e("jingo","Observer 创建了,bean 为null吗?$cityBean") +// cityBean?.let { +// observer?.let { +// Log.e("jingo","Observer 这里为什么要解除观察?") +// downloadData.removeObserver(it) +// } +// Log.e("jingo","Observer 状态 ${cityBean.status} ") +// when (cityBean.status) { +// +// OfflineMapCityBean.PAUSE, OfflineMapCityBean.ERROR, OfflineMapCityBean.NONE -> { +// change(OfflineMapCityBean.WAITING) +// downloadManager.launchScope(this@OfflineMapDownloadScope) +// } +// } +// } +// } +// downloadData.observeForever(observer) +// } + //改进的代码 + fun start() { + change(OfflineMapCityBean.WAITING) + downloadManager.launchScope(this@OfflineMapDownloadScope) + } + + /** + * 暂停任务 + * 其实就是取消任务,移除监听 + */ + fun pause() { + downloadJob?.cancel("pause") + } + + /** + * 启动协程进行下载 + * 请不要尝试在外部调用此方法,那样会脱离[OfflineMapDownloadManager]的管理 + */ + fun launch() { + downloadJob = launch { + try { + download() + change(OfflineMapCityBean.DONE) + } catch (e: Throwable) { + Log.e("jingo DownloadScope", "error:${e.message}") + if (e.message == "pause") { + change(OfflineMapCityBean.PAUSE) + } else { + change(OfflineMapCityBean.ERROR) + } + } finally { + downloadManager.launchNext(cityBean.id) + } + } + } + + + /** + * 是否是等待任务 + */ + fun isWaiting(): Boolean { + val downloadInfo = downloadData.value + downloadInfo ?: return false + return downloadInfo.status == OfflineMapCityBean.WAITING + } + + /** + * 更新任务 + * @param status [OfflineMapCityBean.Status] + */ + private fun change(status: Int) { + downloadData.value?.let { + it.status = status + downloadData.postValue(it) + } + } + + /** + * 添加下载任务观察者 + */ + fun observer(lifecycleOwner: LifecycleOwner, ob: Observer) { + if (observer != null) { + downloadData.removeObserver(observer!!) + } + this.observer = ob + downloadData.observe(lifecycleOwner, observer!!) + } + + /** + * 下载文件 + */ + private suspend fun download() = withContext(context = Dispatchers.IO, block = { + + val downloadInfo = downloadData.value ?: throw IOException("jingo Download info is null") + //创建离线地图 下载文件夹,.map文件夹的下一级 + val fileDir = File("${Constant.OFFLINE_MAP_PATH}download") + if (!fileDir.exists()) { + fileDir.mkdirs() + } + //遍历文件夹,找到对应的省市.map文件 + val files = fileDir.listFiles() + for (item in files) { + //用id找到对应的文件 + if (item.isFile && item.name.startsWith(downloadInfo.id)) { + //判断文件的版本号是否一致 + if (item.name.contains("_${downloadInfo.version}.map")) { + //都一致,说明文件已经下载完成,不用再次下载 + change(OfflineMapCityBean.DONE) + return@withContext + }else{ + + } + break + } + } + + //查看下.map文件夹在不在 + val fileMap = File("${Constant.OFFLINE_MAP_PATH}${downloadInfo.fileName}") + val fileTemp = + File("${Constant.OFFLINE_MAP_PATH}download/${downloadInfo.id}_${downloadInfo.version}") + + + if (fileTemp.exists()) { + + } + + if (!fileMap.exists()) { + } + + change(OfflineMapCityBean.LOADING) + + + val startPosition = downloadInfo.currentSize + //验证断点有效性 + if (startPosition < 0) throw IOException("jingo Start position less than zero") + //下载的文件是否已经被删除 +// if (startPosition > 0 && !TextUtils.isEmpty(downloadInfo.path)) +// if (!File(downloadInfo.path).exists()) throw IOException("File does not exist") + val response = netApi.retrofitDownLoadFile( + start = "bytes=$startPosition-", + url = downloadInfo.url + ) + val responseBody = response.body() + + responseBody ?: throw IOException("jingo ResponseBody is null") + //文件长度 + downloadInfo.fileSize = responseBody.contentLength() + //保存的文件名称 +// if (TextUtils.isEmpty(downloadInfo.fileName)) +// downloadInfo.fileName = UrlUtils.getUrlFileName(downloadInfo.url) + +// //验证下载完成的任务与实际文件的匹配度 +// if (startPosition == downloadInfo.fileSize && startPosition > 0) { +// if (file.exists() && startPosition == file.length()) { +// change(OfflineMapCityBean.DONE) +// return@withContext +// } else throw IOException("jingo The content length is not the same as the file length") +// } + //写入文件 + val randomAccessFile = RandomAccessFile(fileTemp, "rwd") + randomAccessFile.seek(startPosition) +// if (downloadInfo.currentSize == 0L) { +// randomAccessFile.setLength(downloadInfo.fileSize) +// } + downloadInfo.currentSize = startPosition + val inputStream = responseBody.byteStream() + val bufferSize = 1024 * 2 + val buffer = ByteArray(bufferSize) + try { + var readLength = 0 + while (isActive) { + readLength = inputStream.read(buffer) + if (readLength != -1) { + randomAccessFile.write(buffer, 0, readLength) + downloadInfo.currentSize += readLength + change(OfflineMapCityBean.LOADING) + } else { + break + } + } + } finally { + inputStream.close() + randomAccessFile.close() + } + }) + + /** + * + */ + private fun checkFile(){ + + } + } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginActivity.kt b/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginActivity.kt index f4fc2611..36c50b0b 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginActivity.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginActivity.kt @@ -7,6 +7,7 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.navinfo.omqs.R import com.navinfo.omqs.databinding.ActivityLoginBinding @@ -30,41 +31,49 @@ class LoginActivity : PermissionsActivity() { initView() } - private fun initView() { - //登录校验,初始化成功 - viewModel.loginStatus.observe(this) { - when (it) { - LoginStatus.LOGIN_STATUS_NET_LOADING -> { - loginDialog("验证用户信息...") - } - LoginStatus.LOGIN_STATUS_FOLDER_INIT -> { - loginDialog("检查本地数据...") - } - LoginStatus.LOGIN_STATUS_FOLDER_FAILURE -> { - Toast.makeText(this, "文件夹初始化失败", Toast.LENGTH_SHORT).show() - loginDialog?.dismiss() - loginDialog = null - } - LoginStatus.LOGIN_STATUS_NET_FAILURE -> { - Toast.makeText(this, "网络访问失败", Toast.LENGTH_SHORT).show() - loginDialog?.dismiss() - loginDialog = null - } - LoginStatus.LOGIN_STATUS_SUCCESS -> { - val intent = Intent(this@LoginActivity, MainActivity::class.java) - startActivity(intent) + /** + * 观察登录状态,把Observer提出来是为了防止每次数据变化都会有新的observer创建 + * 还有为了方便释放(需不需要手动释放?不清楚)不需要释放,当viewmodel观察到activity/fragment 的生命周期时会自动释放 + * PS:不要在 observer 中修改 LiveData 的值的数据,会影响其他 observer + */ + private val loginObserve = Observer { + when (it) { + LoginStatus.LOGIN_STATUS_NET_LOADING -> { + loginDialog("验证用户信息...") + } + LoginStatus.LOGIN_STATUS_FOLDER_INIT -> { + loginDialog("检查本地数据...") + } + LoginStatus.LOGIN_STATUS_FOLDER_FAILURE -> { + Toast.makeText(this, "文件夹初始化失败", Toast.LENGTH_SHORT).show() + loginDialog?.dismiss() + loginDialog = null + } + LoginStatus.LOGIN_STATUS_NET_FAILURE -> { + Toast.makeText(this, "网络访问失败", Toast.LENGTH_SHORT).show() + loginDialog?.dismiss() + loginDialog = null + } + LoginStatus.LOGIN_STATUS_SUCCESS -> { + val intent = Intent(this@LoginActivity, MainActivity::class.java) + startActivity(intent) // finish() - loginDialog?.dismiss() - loginDialog = null - } - LoginStatus.LOGIN_STATUS_CANCEL -> { - loginDialog?.dismiss() - loginDialog = null - } + loginDialog?.dismiss() + loginDialog = null + } + LoginStatus.LOGIN_STATUS_CANCEL -> { + loginDialog?.dismiss() + loginDialog = null } } } + private fun initView() { + //登录校验,初始化成功 + viewModel.loginStatus.observe(this, loginObserve) + + } + /** * 登录dialog */ @@ -73,7 +82,7 @@ class LoginActivity : PermissionsActivity() { loginDialog = MaterialAlertDialogBuilder( this, com.google.android.material.R.style.MaterialAlertDialog_Material3 ).setTitle("登录").setMessage(message).show() - loginDialog!!.setCanceledOnTouchOutside(true) + loginDialog!!.setCanceledOnTouchOutside(false) loginDialog!!.setOnCancelListener { viewModel.cancelLogin() } @@ -84,13 +93,17 @@ class LoginActivity : PermissionsActivity() { //进应用根本不调用,待查 override fun onPermissionsGranted() { - Log.e("jingo","调用了吗") + Log.e("jingo", "调用了吗") } override fun onPermissionsDenied() { } + override fun onDestroy() { + super.onDestroy() + } + /** * 处理登录按钮 */ diff --git a/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginViewModel.kt b/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginViewModel.kt index 2d6f29b6..ad02cc83 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginViewModel.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginViewModel.kt @@ -5,6 +5,7 @@ import android.util.Log import android.view.View import android.widget.Toast import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.navinfo.omqs.Constant @@ -65,7 +66,7 @@ class LoginViewModel( */ fun onClick(view: View) { loginUser.value!!.username = "admin2" - loginUser.postValue(loginUser.value) + loginUser.value = loginUser.value } /** @@ -81,7 +82,6 @@ class LoginViewModel( //不指定IO,会在主线程里运行 jobLogin = viewModelScope.launch(Dispatchers.IO) { loginCheck(context, userName, password) - Log.e("jingo", "运行完了1?${Thread.currentThread().name}") } } @@ -90,14 +90,12 @@ class LoginViewModel( */ private suspend fun loginCheck(context: Context, userName: String, password: String) { - Log.e("jingo", "我在哪个线程里?${Thread.currentThread().name}") //上面调用了线程切换,这里不用调用,即使调用了还是在同一个线程中,除非自定义协程域?(待验证) // withContext(Dispatchers.IO) { - Log.e("jingo", "delay之前?${Thread.currentThread().name}") //网络访问 loginStatus.postValue(LoginStatus.LOGIN_STATUS_NET_LOADING) - //假装网络访问,等待3秒 - delay(3000) + //假装网络访问,等待2秒 + delay(1000) //文件夹初始化 try { loginStatus.postValue(LoginStatus.LOGIN_STATUS_FOLDER_INIT) @@ -108,7 +106,6 @@ class LoginViewModel( //假装解压文件等 delay(1000) loginStatus.postValue(LoginStatus.LOGIN_STATUS_SUCCESS) - Log.e("jingo", "delay之后?${Thread.currentThread().name}") // } } @@ -121,6 +118,7 @@ class LoginViewModel( sdCardPath?.let { Constant.ROOT_PATH = sdCardPath.absolutePath Constant.MAP_PATH = Constant.ROOT_PATH + "/map/" + Constant.OFFLINE_MAP_PATH = Constant.MAP_PATH + "offline/" val file = File(Constant.MAP_PATH) if (!file.exists()) { file.mkdirs() @@ -135,7 +133,7 @@ class LoginViewModel( Log.e("jingo", "取消了?${Thread.currentThread().name}") jobLogin?.let { it.cancel() - loginStatus.postValue(LoginStatus.LOGIN_STATUS_CANCEL) + loginStatus.value = LoginStatus.LOGIN_STATUS_CANCEL } } diff --git a/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainActivity.kt b/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainActivity.kt index cb03179e..0ec5416c 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainActivity.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainActivity.kt @@ -11,6 +11,7 @@ import com.navinfo.collect.library.map.NIMapController import com.navinfo.omqs.Constant import com.navinfo.omqs.R import com.navinfo.omqs.databinding.ActivityMainBinding +import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager import com.navinfo.omqs.ui.activity.BaseActivity import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -27,6 +28,8 @@ class MainActivity : BaseActivity() { //注入地图控制器 @Inject lateinit var mapController: NIMapController + @Inject + lateinit var offlineMapDownloadManager: OfflineMapDownloadManager override fun onCreate(savedInstanceState: Bundle?) { WindowCompat.setDecorFitsSystemWindows(window, false) diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapAdapter.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapAdapter.kt index 4c1e6786..f3c863a4 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapAdapter.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapAdapter.kt @@ -3,6 +3,7 @@ package com.navinfo.omqs.ui.fragment.offlinemap import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter +import dagger.hilt.EntryPoint /** * 离线地图主页面,viewpage适配器 diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListAdapter.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListAdapter.kt index 8f94464c..8894d104 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListAdapter.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListAdapter.kt @@ -1,33 +1,118 @@ package com.navinfo.omqs.ui.fragment.offlinemap -import androidx.databinding.ViewDataBinding +import android.content.Context +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer import com.navinfo.omqs.R -import com.navinfo.omqs.BR import com.navinfo.omqs.bean.OfflineMapCityBean import com.navinfo.omqs.databinding.AdapterOfflineMapCityBinding +import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager import com.navinfo.omqs.ui.other.BaseRecyclerViewAdapter import com.navinfo.omqs.ui.other.BaseViewHolder import javax.inject.Inject /** * 离线地图城市列表 RecyclerView 适配器 + * + * 在 RecycleView 的 ViewHolder 中监听 ViewModel 的 LiveData,然后此时传递的 lifecycleOwner 是对应的 Fragment。由于 ViewHolder 的生命周期是比 Fragment 短的,所以当 ViewHolder 销毁时,由于 Fragment 的 Lifecycle 还没有结束,此时 ViewHolder 会发生内存泄露(监听的 LiveData 没有解绑) + * 这种场景下有两种解决办法: + *使用 LiveData 的 observeForever 然后在 ViewHolder 销毁前手动调用 removeObserver + *使用 LifecycleRegistry 给 ViewHolder 分发生命周期(这里使用了这个) */ +class OfflineMapCityListAdapter @Inject constructor( + private val downloadManager: OfflineMapDownloadManager, private val context: Context +) : BaseRecyclerViewAdapter() { -class OfflineMapCityListAdapter @Inject constructor() : - BaseRecyclerViewAdapter() { - override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { - var binding: ViewDataBinding = holder.dataBinding - //立刻刷新UI,解决闪烁 -// binding.executePendingBindings() - binding.setVariable(BR.cityBean, data[position]) - (binding as AdapterOfflineMapCityBinding).offlineMapDownloadBtn.setOnClickListener { + private val downloadBtnClick = View.OnClickListener() { + if (it.tag != null) { + val cityBean = data[it.tag as Int] + when (cityBean.status) { + OfflineMapCityBean.NONE, OfflineMapCityBean.UPDATE, OfflineMapCityBean.PAUSE, OfflineMapCityBean.ERROR -> { + downloadManager.start(cityBean.id) + } + OfflineMapCityBean.LOADING, OfflineMapCityBean.WAITING -> { + downloadManager.pause(cityBean.id) + } +// OfflineMapCityBean.WAITING->{ +// downloadManager.cancel(cityBean.id) +// } + } } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + val viewBinding = + AdapterOfflineMapCityBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BaseViewHolder(viewBinding) + } + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + val binding: AdapterOfflineMapCityBinding = + holder.viewBinding as AdapterOfflineMapCityBinding + //牺牲性能立刻刷新UI,解决闪烁 这里不用 +// binding.executePendingBindings() + val cityBean = data[position] + binding.offlineMapDownloadBtn.tag = position + binding.offlineMapDownloadBtn.setOnClickListener(downloadBtnClick) + binding.offlineMapCityName.text = cityBean.name + binding.offlineMapCitySize.text = cityBean.getFileSizeText() + downloadManager.addTask(cityBean) + changeViews(binding, cityBean) + downloadManager.observer(cityBean.id, holder) { + if (cityBean.id == it.id) + changeViews(binding, it) + } + } + + private fun changeViews(binding: AdapterOfflineMapCityBinding, cityBean: OfflineMapCityBean) { + binding.offlineMapProgress.progress = + (cityBean.currentSize * 100 / cityBean.fileSize).toInt() + when (cityBean.status) { + OfflineMapCityBean.NONE -> { + if (binding.offlineMapProgress.visibility == View.VISIBLE) binding.offlineMapProgress.visibility = + View.INVISIBLE + binding.offlineMapDownloadBtn.text = "下载" + } + OfflineMapCityBean.WAITING -> { + if (binding.offlineMapProgress.visibility != View.VISIBLE) binding.offlineMapProgress.visibility = + View.VISIBLE + binding.offlineMapDownloadBtn.text = "等待中" + } + OfflineMapCityBean.LOADING -> { + if (binding.offlineMapProgress.visibility != View.VISIBLE) binding.offlineMapProgress.visibility = + View.VISIBLE + binding.offlineMapDownloadBtn.text = "暂停" + } + OfflineMapCityBean.PAUSE -> { + if (binding.offlineMapProgress.visibility != View.VISIBLE) binding.offlineMapProgress.visibility = + View.VISIBLE + binding.offlineMapDownloadBtn.text = "继续" + } + OfflineMapCityBean.ERROR -> { + if (binding.offlineMapProgress.visibility != View.VISIBLE) binding.offlineMapProgress.visibility = + View.VISIBLE + binding.offlineMapDownloadBtn.text = "重试" + } + OfflineMapCityBean.DONE -> { + if (binding.offlineMapProgress.visibility == View.VISIBLE) binding.offlineMapProgress.visibility = + View.INVISIBLE + binding.offlineMapDownloadBtn.text = "已完成" + } + OfflineMapCityBean.UPDATE -> { + if (binding.offlineMapProgress.visibility == View.VISIBLE) binding.offlineMapProgress.visibility = + View.INVISIBLE + binding.offlineMapDownloadBtn.text = "更新" + } + } } override fun getItemViewType(position: Int): Int { return R.layout.adapter_offline_map_city } +} + -} \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListFragment.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListFragment.kt index 23b74da5..1baa5823 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListFragment.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListFragment.kt @@ -10,17 +10,28 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import com.navinfo.omqs.databinding.FragmentOfflineMapCityListBinding +import com.navinfo.omqs.http.RetrofitNetworkServiceAPI +import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject /** * 离线地图城市列表 */ @AndroidEntryPoint class OfflineMapCityListFragment : Fragment() { + @Inject + lateinit var downloadManager: OfflineMapDownloadManager private var _binding: FragmentOfflineMapCityListBinding? = null private val viewModel by viewModels() private val binding get() = _binding!! - private val adapter: OfflineMapCityListAdapter by lazy { OfflineMapCityListAdapter() } + private val adapter: OfflineMapCityListAdapter by lazy { + OfflineMapCityListAdapter( + downloadManager, + requireContext() + ) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -33,8 +44,10 @@ class OfflineMapCityListFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val layoutManager = LinearLayoutManager(context) - _binding!!.offlineMapCityListRecyclerview.layoutManager = layoutManager - _binding!!.offlineMapCityListRecyclerview.adapter = adapter + //// 设置 RecyclerView 的固定大小,避免在滚动时重新计算视图大小和布局,提高性能 + binding.offlineMapCityListRecyclerview.setHasFixedSize(true) + binding.offlineMapCityListRecyclerview.layoutManager = layoutManager + binding.offlineMapCityListRecyclerview.adapter = adapter viewModel.cityListLiveData.observe(viewLifecycleOwner) { adapter.refreshData(it) } @@ -44,6 +57,6 @@ class OfflineMapCityListFragment : Fragment() { override fun onDestroyView() { super.onDestroyView() _binding = null - Log.e("jingo","OfflineMapCityListFragment onDestroyView") + Log.e("jingo", "OfflineMapCityListFragment onDestroyView") } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListViewModel.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListViewModel.kt index 4eba17ae..93ddba13 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListViewModel.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListViewModel.kt @@ -32,7 +32,7 @@ class OfflineMapCityListViewModel @Inject constructor( viewModelScope.launch { when (val result = networkService.getOfflineMapCityList()) { is NetResult.Success -> { - cityListLiveData.postValue(result.data!!) + cityListLiveData.postValue(result.data?.sortedBy { bean -> bean.id }) } is NetResult.Error -> { Toast.makeText(context, "${result.exception.message}", Toast.LENGTH_SHORT) diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapViewHolder.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapViewHolder.kt new file mode 100644 index 00000000..02a03583 --- /dev/null +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapViewHolder.kt @@ -0,0 +1,10 @@ +package com.navinfo.omqs.ui.fragment.offlinemap + +import com.navinfo.omqs.databinding.AdapterOfflineMapCityBinding +import com.navinfo.omqs.ui.other.BaseViewHolder + +class OfflineMapViewHolder(dataBinding: AdapterOfflineMapCityBinding) : BaseViewHolder(dataBinding) { + init{ + dataBinding.offlineMapDownloadBtn + } +} \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/other/BaseRecyclerViewAdapter.kt b/app/src/main/java/com/navinfo/omqs/ui/other/BaseRecyclerViewAdapter.kt index 33747c57..fa964626 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/other/BaseRecyclerViewAdapter.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/other/BaseRecyclerViewAdapter.kt @@ -1,6 +1,7 @@ package com.navinfo.omqs.ui.other import android.view.LayoutInflater +import android.view.View.OnClickListener import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.RecyclerView @@ -10,23 +11,51 @@ import androidx.recyclerview.widget.RecyclerView */ abstract class BaseRecyclerViewAdapter(var data: List = listOf()) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { - return BaseViewHolder( - DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - viewType, - parent, - false - ) - ) - } + // private var recyclerView: RecyclerView? = null +// override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { +// +// +// +// return BaseViewHolder( +// DataBindingUtil.inflate( +// LayoutInflater.from(parent.context), +// viewType, +// parent, +// false +// ) +// ) +// } + override fun getItemCount(): Int { return data.size } - fun refreshData(newData:List){ + fun refreshData(newData: List) { this.data = newData this.notifyDataSetChanged() } + + + override fun onViewAttachedToWindow(holder: BaseViewHolder) { + super.onViewAttachedToWindow(holder) + holder.onStart() + } + + override fun onViewDetachedFromWindow(holder: BaseViewHolder) { + super.onViewDetachedFromWindow(holder) + holder.apply { + onStop() + } + } +// +// override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { +// super.onAttachedToRecyclerView(recyclerView) +// this.recyclerView = recyclerView +// } +// +// override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { +// super.onDetachedFromRecyclerView(recyclerView) +// this.recyclerView = null +// } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/other/BaseViewHolder.kt b/app/src/main/java/com/navinfo/omqs/ui/other/BaseViewHolder.kt index a06b61ca..e6f4d62c 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/other/BaseViewHolder.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/other/BaseViewHolder.kt @@ -1,11 +1,54 @@ package com.navinfo.omqs.ui.other -import androidx.databinding.ViewDataBinding +import android.view.View +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding /** * dataBinding viewHolder 基类 + * LifecycleRegistry 这是一个生命周期注册器,继承自 Lifecycle,LifecycleOwner 通过这个类来分发生命周期事件,并在 getLifecycle() 中返回 */ -open class BaseViewHolder(var dataBinding: ViewDataBinding) : - RecyclerView.ViewHolder(dataBinding.root) { +open class BaseViewHolder(val viewBinding: ViewBinding) : + RecyclerView.ViewHolder(viewBinding.root), LifecycleOwner { + private val lifecycleRegistry = LifecycleRegistry(this) + + init { +// dataBinding.lifecycleOwner = this + lifecycleRegistry.currentState = Lifecycle.State.INITIALIZED + lifecycleRegistry.currentState = Lifecycle.State.CREATED + itemView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + // View onDetached 的时候回调 onDestroy() + override fun onViewDetachedFromWindow(v: View) { + itemView.removeOnAttachStateChangeListener(this) + onDestroy() + } + + // View onAttached 的时候回调 onCreate() + override fun onViewAttachedToWindow(v: View) { + onStart() + } + }) + } + + fun onStart() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED // + lifecycleRegistry.currentState = Lifecycle.State.RESUMED // ON_RESUME EVENT + } + + fun onStop() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED // + lifecycleRegistry.currentState = Lifecycle.State.CREATED // ON_STOP EVENT + } + + fun onDestroy() { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED /// ON_DESTROY EVENT + } + + + override fun getLifecycle(): Lifecycle { + return lifecycleRegistry + } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/widget/MyProgressBar.kt b/app/src/main/java/com/navinfo/omqs/ui/widget/MyProgressBar.kt index 0f1180a6..aab03188 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/widget/MyProgressBar.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/widget/MyProgressBar.kt @@ -5,10 +5,13 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Rect -import android.opengl.ETC1.getHeight -import android.opengl.ETC1.getWidth import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout import android.widget.ProgressBar +import com.navinfo.omqs.R /** @@ -18,8 +21,13 @@ class MyProgressBar : ProgressBar { private lateinit var mPaint: Paint private var text: String = "" private var rate = 0f + private lateinit var bar: ProgressBar constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { +// LayoutInflater.from(context).inflate( +// R.layout.my_projressbar, this, +// true +// ); initView() } @@ -33,6 +41,7 @@ class MyProgressBar : ProgressBar { mPaint.color = Color.BLUE } + @Synchronized override fun setProgress(progress: Int) { setText(progress) @@ -40,7 +49,7 @@ class MyProgressBar : ProgressBar { } private fun setText(progress: Int) { - rate = progress * 1.0f / this.getMax() + rate = progress * 1.0f / this.max val i = (rate * 100).toInt() text = "$i%" } @@ -57,8 +66,9 @@ class MyProgressBar : ProgressBar { // 如果为百分之百则在左边绘制。 x = width - rect.right } - val y: Int = 0 - rect.top - mPaint.textSize = 22f + mPaint.textSize = 24f + val y: Int = 10 - rect.top + canvas.drawText(text, x.toFloat(), y.toFloat(), mPaint) } } \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_bg.xml b/app/src/main/res/drawable/progress_bg.xml new file mode 100644 index 00000000..244083fc --- /dev/null +++ b/app/src/main/res/drawable/progress_bg.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index d1405c56..1d88e9f5 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -17,8 +17,7 @@ + android:layout_height="match_parent"> - - - - - - - - - + android:text="省市名称" + android:textColor="@color/white" + android:textSize="@dimen/default_font_size" /> - + - - + - + - - - \ No newline at end of file + + \ No newline at end of file diff --git a/collect-library/src/main/java/com/navinfo/collect/library/utils/GeometryTools.java b/collect-library/src/main/java/com/navinfo/collect/library/utils/GeometryTools.java index 4c6d0519..bf7066d2 100644 --- a/collect-library/src/main/java/com/navinfo/collect/library/utils/GeometryTools.java +++ b/collect-library/src/main/java/com/navinfo/collect/library/utils/GeometryTools.java @@ -429,7 +429,6 @@ public class GeometryTools { dList.add(lt + dis); total += dis; } - Log.e("jingo", "line lengh =" + total); total = total / 2; for (int i = 0; i < dList.size(); i++) { double a = dList.get(i); @@ -495,7 +494,6 @@ public class GeometryTools { dList.add(lt + dis); total += dis; } - Log.e("jingo", "line lengh =" + total); total = total / 2; for (int i = 0; i < dList.size(); i++) { double a = dList.get(i);