diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/MXImagePicker.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/MXImagePicker.kt index 0d42b0678a7d2dc8ad049b79c229de7953d66d8c..4e97aabb0d8ba8dbb8b098a6fb512601ff89d1ce 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/MXImagePicker.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/MXImagePicker.kt @@ -14,11 +14,11 @@ import java.util.concurrent.atomic.AtomicBoolean object MXImagePicker { private val hasInit = AtomicBoolean(false) private var application: Application? = null - internal fun init(application: Application) { + fun init(application: Application) { if (hasInit.get()) return + this.application = application application.registerActivityLifecycleCallbacks(activityLifecycleCall) hasInit.set(true) - this.application = application } internal fun getContext() = application!! diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/adapts/FolderAdapt.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/adapts/FolderAdapt.kt index 548ec8e5f46158e39e38dee9a8081c013965b453..fa240f0ed7f4654011e7559a581d681e43da7bbf 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/adapts/FolderAdapt.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/adapts/FolderAdapt.kt @@ -10,6 +10,7 @@ import androidx.recyclerview.widget.RecyclerView import com.mx.imgpicker.MXImagePicker import com.mx.imgpicker.R import com.mx.imgpicker.app.picker.MXPickerVM +import com.mx.imgpicker.db.MXDBSource import com.mx.imgpicker.models.MXDirItem import kotlinx.coroutines.launch @@ -39,7 +40,7 @@ internal class FolderAdapt( if (item.lastItem == null) { lifecycleScope.launch { - item.lastItem = vm.sourceDB.queryLastItem(item.path, vm.pickerType) + item.lastItem = MXDBSource.instance.queryLastItem(item.path, vm.pickerType) this@FolderAdapt.notifyItemChanged(position) } } diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/MXImgPickerActivity.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/MXImgPickerActivity.kt index 01a564daac6e5da4ba1b95c32a472661a16f7b95..b573f4211468286b8ddbc41d6c2ef1f6dbb0dc3b 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/MXImgPickerActivity.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/MXImgPickerActivity.kt @@ -21,6 +21,7 @@ import com.mx.imgpicker.models.MXItem import com.mx.imgpicker.models.MXPickerType import com.mx.imgpicker.observer.MXSysImageObserver import com.mx.imgpicker.observer.MXSysVideoObserver +import com.mx.imgpicker.utils.MXScanBiz import com.mx.imgpicker.utils.MXUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -81,6 +82,11 @@ class MXImgPickerActivity : AppCompatActivity() { } } + override fun onStart() { + super.onStart() + MXScanBiz.scanRecent(this, lifecycleScope) + } + override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, @@ -112,8 +118,10 @@ class MXImgPickerActivity : AppCompatActivity() { vm.selectDirLive.observe(this) { lifecycleScope.launch { vm.reloadMediaList() } } - - vm.startScan() + MXScanBiz.setOnUpdateListener { + lifecycleScope.launch { vm.reloadMediaList() } + } + MXScanBiz.scanAll(this, lifecycleScope) } fun showLargeView(show: Boolean, target: MXItem? = null) { @@ -138,13 +146,13 @@ class MXImgPickerActivity : AppCompatActivity() { private val imageChangeObserver = MXSysImageObserver { if (isDestroyed) return@MXSysImageObserver if (vm.pickerType in arrayOf(MXPickerType.Image, MXPickerType.ImageAndVideo)) { - vm.startScan() + MXScanBiz.scanRecent(this, lifecycleScope) } } private val videoChangeObserver = MXSysVideoObserver { if (isDestroyed) return@MXSysVideoObserver if (vm.pickerType in arrayOf(MXPickerType.Video, MXPickerType.ImageAndVideo)) { - vm.startScan() + MXScanBiz.scanRecent(this, lifecycleScope) } } @@ -179,9 +187,13 @@ class MXImgPickerActivity : AppCompatActivity() { override fun onDestroy() { try { contentResolver.unregisterContentObserver(imageChangeObserver) + } catch (_: Exception) { + } + try { contentResolver.unregisterContentObserver(videoChangeObserver) - } catch (e: Exception) { + } catch (_: Exception) { } + MXScanBiz.setOnUpdateListener(null) super.onDestroy() } diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/MXPickerVM.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/MXPickerVM.kt index e38fdeacc0880330e010a9c2a732516705e2d16b..c2fdf1c2a7beb8b92b51083ad41deb48ea276436 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/MXPickerVM.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/MXPickerVM.kt @@ -8,27 +8,14 @@ import com.mx.imgpicker.R import com.mx.imgpicker.db.MXDBSource import com.mx.imgpicker.models.* import com.mx.imgpicker.utils.MXUtils -import com.mx.imgpicker.utils.source_loader.MXDirSource -import com.mx.imgpicker.utils.source_loader.MXImageSource -import com.mx.imgpicker.utils.source_loader.MXVideoSource import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.math.abs internal class MXPickerVM : ViewModel() { - companion object { - private const val PAGE_SIZE = 40 - } - private val allResStr by lazy { MXImagePicker.getContext().resources.getString(R.string.mx_picker_string_all) } private val allDir by lazy { MXDirItem(allResStr, "", 0) } - val sourceDB by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { - MXDBSource(MXImagePicker.getContext()) - } var pickerType: MXPickerType = MXPickerType.Image // 类型 private set @@ -72,59 +59,21 @@ internal class MXPickerVM : ViewModel() { val needCompress = MutableLiveData(true) // 是否需要压缩 val fullScreenSelectIndex = MutableLiveData(0) // 是否需要压缩 - private val scanLock = AtomicBoolean(false) - private var lastReloadTime = 0L - fun startScan() { - viewModelScope.launch { - if (scanLock.get()) return@launch - scanLock.set(true) - MXUtils.log("开始扫描--> <--") - val context = MXImagePicker.getContext() - val scanResult: ((List) -> Boolean) = { list -> - viewModelScope.launch { - if (list.isEmpty()) return@launch - val hasSave = withContext(Dispatchers.IO) { - mediaList.containsAll(list) - } - if (hasSave) return@launch - sourceDB.addSysSource(list) - if (abs(System.currentTimeMillis() - lastReloadTime) > 3000) { - reloadMediaList() - } - } - this.isActive - } - if (pickerType != MXPickerType.Video) { - MXImageSource.scan(context, PAGE_SIZE, scanResult) - } - if (pickerType != MXPickerType.Image) { - MXVideoSource.scan(context, PAGE_SIZE, scanResult) - } - - val dirs = sourceDB.getAllDirList(pickerType) - MXDirSource(dirs).scan(context, PAGE_SIZE, scanResult) - - reloadMediaList() - MXUtils.log("结束扫描--> <--") - scanLock.set(false) - } - } - fun addPrivateSource(file: File, type: MXPickerType) { - viewModelScope.launch { sourceDB.addPrivateSource(file, type) } + viewModelScope.launch { MXDBSource.instance.addPrivateSource(file, type) } } suspend fun reloadMediaList() = withContext(Dispatchers.IO) { val start = System.currentTimeMillis() val selectDir = selectDirLive.value ?: allDir - val mediaList = sourceDB.getAllSource(pickerType, selectDir.path, maxListSize) + val mediaList = MXDBSource.instance.getAllSource(pickerType, selectDir.path, maxListSize) if (!MXUtils.compareList(_mediaList, mediaList)) { MXUtils.log("刷新->图片列表 ${_mediaList?.size}->${mediaList.size}") _mediaList = mediaList mediaListLive.postValue(Any()) } - val dirs = sourceDB.getAllDirList(pickerType) + val dirs = MXDBSource.instance.getAllDirList(pickerType) allDir.childSize = dirs.sumOf { it.childSize } val allDirs = listOf(allDir) + dirs @@ -134,9 +83,15 @@ internal class MXPickerVM : ViewModel() { dirListLive.postValue(Any()) } MXUtils.log("刷新->加载时长:${(System.currentTimeMillis() - start) / 1000f} 秒") + } - if (mediaList.isNotEmpty()) { - lastReloadTime = System.currentTimeMillis() + suspend fun onMediaInsert(file: File) = withContext(Dispatchers.IO) { + val ext = file.extension?.lowercase() + val type = if (ext in MXUtils.IMAGE_EXT) MXPickerType.Image else MXPickerType.Video + val item = MXItem(file.absolutePath, file.lastModified(), type) + _mediaList = ArrayList(_mediaList ?: emptyList()).apply { + if (!this.contains(item)) add(0, item) } + mediaListLive.postValue(Any()) } } \ No newline at end of file diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/fragment/MXPickerFragment.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/fragment/MXPickerFragment.kt index eb5dd51a0d8324fe7f4f4dd575326dbc464e8a48..3172fa7c8391563e22d20ce687917d1f302050dc 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/fragment/MXPickerFragment.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/app/picker/fragment/MXPickerFragment.kt @@ -26,8 +26,8 @@ import com.mx.imgpicker.builder.MXCaptureBuilder import com.mx.imgpicker.models.MXCompressType import com.mx.imgpicker.models.MXPickerType import com.mx.imgpicker.utils.MXUtils -import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.io.File internal class MXPickerFragment : Fragment() { private val vm by lazy { ViewModelProvider(requireActivity()).get(MXPickerVM::class.java) } @@ -48,6 +48,8 @@ internal class MXPickerFragment : Fragment() { private var willResizeLay: View? = null private var willResizeImg: ImageView? = null + private var targetFile: File? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -132,9 +134,10 @@ internal class MXPickerFragment : Fragment() { val intent = captureBuilder.createIntent(requireContext()) val file = captureBuilder.getCaptureFile() vm.addPrivateSource(file, type) + targetFile = file + MXUtils.log("PATH = ${file.absolutePath}") startActivityForResult(intent, 0x12) - MXUtils.log("PATH = ${file.absolutePath}") } if (vm.pickerType == MXPickerType.ImageAndVideo) { @@ -246,9 +249,11 @@ internal class MXPickerFragment : Fragment() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + val file = targetFile ?: return + if (!file.exists()) return lifecycleScope.launch { - delay(1000) - vm.reloadMediaList() + vm.onMediaInsert(file) } + targetFile = null } } \ No newline at end of file diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/db/MXDBSource.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/db/MXDBSource.kt index 4842fc1a679f590cd87218d30cc7d74b70a1c1cc..005a04c1a9c771307bd22f28460b00669a4bf68d 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/db/MXDBSource.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/db/MXDBSource.kt @@ -4,6 +4,7 @@ import android.content.ContentValues import android.content.Context import android.database.Cursor import android.database.sqlite.SQLiteDatabase +import com.mx.imgpicker.MXImagePicker import com.mx.imgpicker.models.MXDirItem import com.mx.imgpicker.models.MXItem import com.mx.imgpicker.models.MXPickerType @@ -13,9 +14,10 @@ import kotlinx.coroutines.withContext import java.io.File import kotlin.math.abs -internal class MXDBSource(val context: Context) { +internal class MXDBSource private constructor(val context: Context) { companion object { private val lock = Object() + val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { MXDBSource(MXImagePicker.getContext()) } } private val dbHelp by lazy { MXSQLite(context.applicationContext) } @@ -53,41 +55,40 @@ internal class MXDBSource(val context: Context) { /** * 批量添加/替换系统视频、图片数据 */ - suspend fun addSysSource(list: List): Boolean = - withContext(Dispatchers.IO) { - synchronized(lock) { - val database = dbHelp.writableDatabase - val insertSql = - "replace into ${MXSQLite.DB_NAME}(" + - "${MXSQLite.DB_PATH}, " + - "${MXSQLite.DB_DIR}, " + - "${MXSQLite.DB_TYPE}, " + - "${MXSQLite.DB_PRIVATE}, " + - "${MXSQLite.DB_TIME}, " + - "${MXSQLite.DB_VIDEO_LENGTH}) " + - "values(?,?,?,?,?,?)" - val stat = database.compileStatement(insertSql) - database.beginTransaction() - try { - for (item in list) { - stat.bindString(1, item.path) - stat.bindString(2, File(item.path).parentFile?.absolutePath) - stat.bindString(3, item.type.value) - stat.bindString(4, MXSQLite.VALUE_PRIVATE_SYS) - stat.bindLong(5, item.timeInMs) - stat.bindLong(6, item.duration.toLong()) - stat.executeInsert() - } - database.setTransactionSuccessful() - } catch (e: Exception) { - e.printStackTrace() - } finally { - database.endTransaction() - database.close() + fun addSysSource(list: List): Boolean { + synchronized(lock) { + val database = dbHelp.writableDatabase + val insertSql = + "replace into ${MXSQLite.DB_NAME}(" + + "${MXSQLite.DB_PATH}, " + + "${MXSQLite.DB_DIR}, " + + "${MXSQLite.DB_TYPE}, " + + "${MXSQLite.DB_PRIVATE}, " + + "${MXSQLite.DB_TIME}, " + + "${MXSQLite.DB_VIDEO_LENGTH}) " + + "values(?,?,?,?,?,?)" + val stat = database.compileStatement(insertSql) + database.beginTransaction() + try { + for (item in list) { + stat.bindString(1, item.path) + stat.bindString(2, File(item.path).parentFile?.absolutePath) + stat.bindString(3, item.type.value) + stat.bindString(4, MXSQLite.VALUE_PRIVATE_SYS) + stat.bindLong(5, item.timeInMs) + stat.bindLong(6, item.duration.toLong()) + stat.executeInsert() } + database.setTransactionSuccessful() + } catch (e: Exception) { + e.printStackTrace() + } finally { + database.endTransaction() + database.close() } - return@withContext false } + return false + } /** * 获取对应类型的所有数据 diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/models/beans.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/models/beans.kt index a845621800ce008d5d8c1c6df4e92d1f01332cee..d70f3db1135f802f012fd72d69c32589b5b50a96 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/models/beans.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/models/beans.kt @@ -114,4 +114,4 @@ internal data class MXDirItem( override fun toString(): String { return "MXDirItem(name='$name', path='$path', childSize=$childSize)" } -} +} \ No newline at end of file diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/MXScanBiz.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/MXScanBiz.kt new file mode 100644 index 0000000000000000000000000000000000000000..695549fed9d388c2e7ee6a7b62fa131d50cb3bbd --- /dev/null +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/MXScanBiz.kt @@ -0,0 +1,138 @@ +package com.mx.imgpicker.utils + +import android.content.Context +import com.mx.imgpicker.db.MXDBSource +import com.mx.imgpicker.models.MXPickerType +import com.mx.imgpicker.utils.source_loader.MXDirSource +import com.mx.imgpicker.utils.source_loader.MXImageSource +import com.mx.imgpicker.utils.source_loader.MXVideoSource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.math.min + +object MXScanBiz { + private val lock = Object() + private const val PAGE_SIZE = 20 + private const val SCAN_SIZE = PAGE_SIZE * 2 + + private var hasScanAllImage = false + private var hasScanAllVideo = false + private var hasScanAllDirs = false + private var listener: (() -> Unit)? = null + + fun scanAll(context: Context, scope: CoroutineScope) { + MXUtils.log("MXScanBiz -- scanAll") + val job = scope.launch { + MXUtils.log("MXScanBiz -- scanAll 开始扫描") + if (!hasScanAllImage) { + val size = scanImage(context, Int.MAX_VALUE) + if (size > 0) { + hasScanAllImage = true + } + } + if (!hasScanAllVideo) { + val size = scanVideo(context, Int.MAX_VALUE) + if (size > 0) { + hasScanAllVideo = true + } + } + if (!hasScanAllDirs) { + val size = scanDirs(context, Int.MAX_VALUE) + if (size > 0) { + hasScanAllDirs = true + } + } + } + job.invokeOnCompletion { + MXUtils.log("MXScanBiz -- scanAll 结束扫描") + listener?.invoke() + } + } + + fun scanRecent(context: Context, scope: CoroutineScope) { + MXUtils.log("MXScanBiz -- scanRecent") + val job = scope.launch { + MXUtils.log("MXScanBiz -- 开始扫描") + scanImage(context, SCAN_SIZE) + scanVideo(context, SCAN_SIZE) + } + job.invokeOnCompletion { + MXUtils.log("MXScanBiz -- 结束扫描") + listener?.invoke() + } + } + + internal fun setOnUpdateListener(call: (() -> Unit)?) { + listener = call + } + + private suspend fun scanImage(context: Context, scanSize: Int) = withContext(Dispatchers.IO) { + var sumSize = 0 + var currentScanSize = 0 + var currentScanPage = 0 + val scanPageSize = min(PAGE_SIZE, scanSize) + do { + val list = synchronized(lock) { + MXImageSource.scan(context, scanPageSize, sumSize) + } ?: break + currentScanSize = list.size + sumSize += currentScanSize + MXDBSource.instance.addSysSource(list) + + currentScanPage++ + if (currentScanPage < 3 || currentScanPage % 10 == 0) { + listener?.invoke() + } + } while (currentScanSize > 0 && sumSize < scanSize) + + MXUtils.log("MXScanBiz -- scanImage 结束扫描 -->${sumSize}") + return@withContext sumSize + } + + private suspend fun scanVideo(context: Context, scanSize: Int) = withContext(Dispatchers.IO) { + var sumSize = 0 + var currentScanSize = 0 + var currentScanPage = 0 + val scanPageSize = min(PAGE_SIZE, scanSize) + do { + val list = synchronized(lock) { + MXVideoSource.scan(context, scanPageSize, sumSize) + } ?: break + currentScanSize = list.size + sumSize += currentScanSize + MXDBSource.instance.addSysSource(list) + + currentScanPage++ + if (currentScanPage < 3 || currentScanPage % 10 == 0) { + listener?.invoke() + } + } while (currentScanSize > 0 && sumSize < scanSize) + MXUtils.log("MXScanBiz -- scanVideo 结束扫描 -->${sumSize}") + return@withContext sumSize + } + + private suspend fun scanDirs(context: Context, scanSize: Int) = withContext(Dispatchers.IO) { + val dirs = MXDBSource.instance.getAllDirList(MXPickerType.ImageAndVideo) + var sumSize = 0 + var currentScanSize = 0 + var currentScanPage = 0 + val scanPageSize = min(PAGE_SIZE, scanSize) + do { + val list = synchronized(lock) { + MXDirSource(dirs).scan(context, scanPageSize, sumSize) + } ?: break + currentScanSize = list.size + sumSize += currentScanSize + MXDBSource.instance.addSysSource(list) + + currentScanPage++ + if (currentScanPage < 3 || currentScanPage % 10 == 0) { + listener?.invoke() + } + } while (currentScanSize > 0 && sumSize < scanSize) + MXUtils.log("MXScanBiz -- scanDirs 结束扫描 -->${sumSize}") + return@withContext sumSize + } +} \ No newline at end of file diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/IMXSource.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/IMXSource.kt index 57ca437c3d8860c11abf60926fe2fed445c43469..4e4f2b002bc3ca62d7cf52f0ab842689db8defbe 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/IMXSource.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/IMXSource.kt @@ -5,11 +5,7 @@ import com.mx.imgpicker.models.MXItem import java.io.File internal interface IMXSource { - suspend fun scan( - context: Context, - pageSize: Int, - onScanCall: ((List) -> Boolean) - ) + fun scan(context: Context, size: Int, offset: Int): List? fun save(context: Context, file: File): Boolean } \ No newline at end of file diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXDirSource.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXDirSource.kt index d65e0f23fe1098dfd0f3ebfecf1a6a3eae883e21..21610b13998b71332237c14d1c1cf8780b81ab32 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXDirSource.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXDirSource.kt @@ -5,52 +5,38 @@ import com.mx.imgpicker.models.MXDirItem import com.mx.imgpicker.models.MXItem import com.mx.imgpicker.models.MXPickerType import com.mx.imgpicker.utils.MXUtils -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import java.io.File internal class MXDirSource(private val dirs: List) : IMXSource { - override suspend fun scan( - context: Context, - pageSize: Int, - onScanCall: (List) -> Boolean - ) = withContext(Dispatchers.IO) { - if (dirs.isEmpty()) return@withContext + override fun scan(context: Context, size: Int, offset: Int): List? { + if (dirs.isEmpty()) return null val list = ArrayList() + var index = 0 for (dir in dirs) { - val files = File(dir.path).listFiles() + val files = File(dir.path).listFiles()?.sortedBy { it.name } if (files == null || files.isEmpty()) continue for (file in files) { - val ext = file.extension?.lowercase() - if (ext in MXUtils.IMAGE_EXT) { - list.add( - MXItem( - file.absolutePath, - file.lastModified(), - MXPickerType.Image - ) - ) - } else if (ext in MXUtils.VIDEO_EXT) { - list.add( - MXItem( - file.absolutePath, - file.lastModified(), - MXPickerType.Video - ) - ) + val item = when (file.extension.lowercase()) { + in MXUtils.IMAGE_EXT -> { + MXItem(file.absolutePath, file.lastModified(), MXPickerType.Image) + } + in MXUtils.VIDEO_EXT -> { + MXItem(file.absolutePath, file.lastModified(), MXPickerType.Video) + } + else -> null } - if (list.size >= pageSize) { - val continueScan = onScanCall.invoke(list.toList()) - list.clear() - if (!continueScan) { - return@withContext + if (item != null) { + index++ + if (index > offset) { + list.add(item) + } + if (list.size >= size) { + return list } } } } - if (list.isNotEmpty()) { - onScanCall.invoke(list.toList()) - } + return list } override fun save(context: Context, file: File): Boolean { diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXImageSource.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXImageSource.kt index 25a1f46500075e60e22769328fd4e211c991c9ba..1e3bf506024fb55c63a425804fe8114c1b4c6ff3 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXImageSource.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXImageSource.kt @@ -11,8 +11,6 @@ import android.os.Build import android.provider.MediaStore import com.mx.imgpicker.models.MXItem import com.mx.imgpicker.models.MXPickerType -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import java.io.File @@ -20,13 +18,10 @@ internal object MXImageSource : IMXSource { const val MIME_TYPE = "image/*" private val SOURCE_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI - override suspend fun scan( - context: Context, - pageSize: Int, - onScanCall: ((List) -> Boolean) - ) = withContext(Dispatchers.IO) { + override fun scan(context: Context, size: Int, offset: Int): List? { + val images = ArrayList() //扫描图片 - val resolver = context.contentResolver ?: return@withContext + val resolver = context.contentResolver ?: return null val columns = arrayListOf( MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID, @@ -39,7 +34,6 @@ internal object MXImageSource : IMXSource { val where = MediaStore.Images.Media.SIZE + " > ? " val whereArgs = arrayListOf("0") - val images = ArrayList() var mCursor: Cursor? = null try { mCursor = MXContentProvide.createCursor( @@ -50,28 +44,33 @@ internal object MXImageSource : IMXSource { ) if (mCursor == null || !mCursor.moveToFirst()) { - return@withContext + return null + } + if (offset > 0) { + if (offset < mCursor.count) { + mCursor.move(offset) + } else { + return null + } } do { val item = cursorToImageItem(resolver, mCursor) if (item != null) { images.add(item) } - if (images.size >= pageSize) { - val scanContinue = onScanCall.invoke(images.toList()) - images.clear() - if (!scanContinue) break + if (images.size >= size) { + break } } while (mCursor.moveToNext()) - onScanCall.invoke(images.toList()) } catch (e: Exception) { e.printStackTrace() } finally { try { mCursor?.close() - } catch (e: Exception) { + } catch (_: Exception) { } } + return images } private fun cursorToImageItem(contentResolver: ContentResolver, mCursor: Cursor): MXItem? { diff --git a/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXVideoSource.kt b/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXVideoSource.kt index 421cd2375b6950ea884f1aca1d4c774d6f818f27..0847c89d4a2f8b60a4770a86e4cae33cbeb80882 100644 --- a/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXVideoSource.kt +++ b/ImagePickerLib/src/main/java/com/mx/imgpicker/utils/source_loader/MXVideoSource.kt @@ -11,20 +11,15 @@ import android.os.Build import android.provider.MediaStore import com.mx.imgpicker.models.MXItem import com.mx.imgpicker.models.MXPickerType -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import java.io.File internal object MXVideoSource : IMXSource { const val MIME_TYPE = "video/*" private val SOURCE_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI - override suspend fun scan( - context: Context, - pageSize: Int, - onScanCall: ((List) -> Boolean) - ) = withContext(Dispatchers.IO) { + override fun scan(context: Context, size: Int, offset: Int): List? { + val images = ArrayList() //扫描图片 - val resolver = context.contentResolver ?: return@withContext + val resolver = context.contentResolver ?: return null val columns = arrayListOf( MediaStore.Video.Media.DATA, MediaStore.Video.Media._ID, @@ -38,7 +33,6 @@ internal object MXVideoSource : IMXSource { val where = MediaStore.Video.Media.SIZE + ">?" val whereArgs = arrayListOf("0") - val images = ArrayList() var mCursor: Cursor? = null try { mCursor = MXContentProvide.createCursor( @@ -48,28 +42,33 @@ internal object MXVideoSource : IMXSource { false ) if (mCursor == null || !mCursor.moveToFirst()) { - return@withContext + return null + } + if (offset > 0) { + if (offset < mCursor.count) { + mCursor.move(offset) + } else { + return null + } } do { val item = cursorToImageItem(resolver, mCursor) if (item != null) { images.add(item) } - if (images.size >= pageSize) { - val scanContinue = onScanCall.invoke(images.toList()) - images.clear() - if (!scanContinue) break + if (images.size >= size) { + break } } while (mCursor.moveToNext()) - onScanCall.invoke(images.toList()) } catch (e: Exception) { e.printStackTrace() } finally { try { mCursor?.close() - } catch (e: Exception) { + } catch (_: Exception) { } } + return images } private fun cursorToImageItem(contentResolver: ContentResolver, mCursor: Cursor): MXItem? { diff --git a/README.md b/README.md index 06c0068243e9b23e12da4ef9707a6b9764fdd27b..3d055cede9b933af358a41ff3fe1c6677c8c99f1 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,24 @@ # ImagePicker + ## 介绍 + 基于Kotlin,AndroidX的仿微信图片选择器 [![](https://jitpack.io/v/com.gitee.zhangmengxiong/MXImagePicker.svg)](https://jitpack.io/#com.gitee.zhangmengxiong/MXImagePicker) --- -引用方法1 - Gradle引用 +Gradle引用 + ```gradle - implementation 'com.gitee.zhangmengxiong:MXImagePicker:1.5.7' + implementation 'com.gitee.zhangmengxiong:MXImagePicker:1.5.8' ``` ---- -引用方法2 - AAR包引用 -- [下载aar - 1.5.7版本](https://gitee.com/zhangmengxiong/MXImagePicker/releases/download/1.5.7/ImagePickerLib-1.5.7.aar) ---- ![Image text](https://gitee.com/zhangmengxiong/MXImagePicker/raw/master/imgs/screenshot1.png) ![Image text](https://gitee.com/zhangmengxiong/MXImagePicker/raw/master/imgs/screenshot2.png) +![Image text](https://gitee.com/zhangmengxiong/MXImagePicker/raw/master/imgs/screenshot3.png) ## 使用方法 #### 第一步:项目增加Androidx库和Glide图片加载库、图片缩放库 + ```gradle implementation "androidx.appcompat:appcompat:x.x.x" implementation "androidx.recyclerview:recyclerview:x.x.x" @@ -27,83 +28,103 @@ ``` #### 第二步:使用前需要修改‘AndroidManifest.xml’配置:添加相册、存储权限 + ```kotlin Manifest.permission.CAMERA - Manifest.permission.READ_EXTERNAL_STORAGE +Manifest.permission.READ_EXTERNAL_STORAGE - // targetSdkVersion >= 29 的应用需要在application节点添加以下属性 - android:requestLegacyExternalStorage="true" +// targetSdkVersion >= 29 的应用需要在application节点添加以下属性 +android:requestLegacyExternalStorage = "true" ``` + 注意:`没有权限进入选择页面会报错!` #### 第三步:启动选择页面 + ```kotlin val intent = MXPickerBuilder().setMaxSize(3).createIntent(this) -startActivityForResult(intent,0x22) +startActivityForResult(intent, 0x22) +``` + +##### 预加载说明 + +预加载可以提前搜索本机图片/视频资源,减少首次进入选择页面时空白时间 + +```kotlin +MXImagePicker.init(application) +MXScanBiz.scanAll(this, lifecycleScope) ``` + ##### MXPickerBuilder参数说明 + 1. `setMaxSize(size: Int)` 设置最大选择文件个数 -2. `setType(type: PickerType)` 设置类型 +2. `setType(type: PickerType)` 设置类型 * PickerType.Image = 图片 * PickerType.Video = 视频 - * PickerType.ImageAndVideo = 图片 + 视频 混合选择 + * PickerType.ImageAndVideo = 图片 + 视频 混合选择 3. `setCameraEnable(enable: Boolean)` 设置是否启动拍摄功能,默认=true -4. `setMaxVideoLength(length: Int)` 当类型=Video时,可以选择视频最大时长限制,单位:秒 默认=-1 无限制 -5. `setMaxListSize(size: Int)` 最长列表加载长度,防止图片过多时产生OOM -1=不限制 默认限制长度=1000条 +4. `setMaxVideoLength(length: Int)` 当类型=Video时,可以选择视频最大时长限制,单位:秒 默认=-1 无限制 +5. `setMaxListSize(size: Int)` 最长列表加载长度,防止图片过多时产生OOM -1=不限制 默认限制长度=1000条 ```kotlin // 在图片选择器Activity创建时会回调这个方法,一般会通过这个来改变导航栏、状态栏的Theme,demo中搭配`ImmersionBar`来实现沉浸式效果 MXImagePicker.registerActivityCallback { activity -> ImmersionBar.with(activity) - .autoDarkModeEnable(true) - .statusBarColorInt(activity.resources.getColor(R.color.picker_color_background)) - .fitsSystemWindows(true) - .navigationBarColor(R.color.picker_color_background) - .init() + .autoDarkModeEnable(true) + .statusBarColorInt(activity.resources.getColor(R.color.picker_color_background)) + .fitsSystemWindows(true) + .navigationBarColor(R.color.picker_color_background) + .init() } ``` ##### 页面颜色设置 + 将下面颜色值放如主项目的资源xml中,可以修改页面对应的颜色显示 + ```xml - #333333 - - - #F1F1F1 +#333333 + + +#F1F1F1 - - #03CE65 + +#03CE65 ``` ##### 多语言设置 + 将下面字符串定义放入对应的语言目录中,可以修改页面对应的文字提示 + ```xml - 选择 - 全部 - 您最多只能选择 %s 张图片! - 您最多只能选择 %s 个视频! - 只能选择 %s 秒以内的视频 - 需要写入存储、相机权限 - 需要读取存储权限 - 打开失败! - 预览 - 原图 - 拍摄图片 - 拍摄视频 - 图片查看 + 选择 + 全部 + 您最多只能选择 %s 张图片! + 您最多只能选择 %s 个视频! + 只能选择 %s 秒以内的视频 + 需要写入存储、相机权限 + 需要读取存储权限 + 打开失败! + 预览 + 原图 + 拍摄图片 + 拍摄视频 + 图片查看 ``` dimens.xml 资源 + ```xml - - 50dp + +50dp ``` ##### 自定义图片加载器(默认使用Glide) 通过继承实现接口`IImageLoader` ,并注册到服务`MXImagePicker`即可 + ```kotlin // 数据对象 data class MXItem(val path: String, val time: Long, val type: MXPickerType, val duration: Int = 0) @@ -124,6 +145,7 @@ MXImagePicker.registerImageLoader { activity, item, imageView -> ``` #### 第四步:获取返回结果 + ```kotlin override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) @@ -134,9 +156,8 @@ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) } ``` - - ### 调取摄像头单独拍摄照片 + ```kotlin val builder = MXCaptureBuilder().setType(MXPickerType.Image) @@ -145,7 +166,9 @@ startActivityForResult(builder.createIntent(this), 0x11) // 在onActivityResult获取结果 val file = builder.getCaptureFile() ``` + ### 调取摄像头单独拍摄视频 + ```kotlin val builder = MXCaptureBuilder().setType(MXPickerType.Video).setMaxVideoLength(10) startActivityForResult(builder.createIntent(this), 0x11) @@ -154,9 +177,10 @@ startActivityForResult(builder.createIntent(this), 0x11) val file = builder.getCaptureFile() ``` - ### 图片查看器 + ![Image text](https://gitee.com/zhangmengxiong/MXImagePicker/raw/master/imgs/screenshot3.png) + ```kotlin MXImgShowActivity.open( this, arrayListOf( @@ -167,6 +191,7 @@ MXImgShowActivity.open( ``` ### 单张图片压缩 + ```kotlin val file = File(".../xx.png") val scaleImg = MXImageCompress.from(context) diff --git a/app/build.gradle b/app/build.gradle index e2856d93729fd3aa0d000f43bb3d46c88606983b..b2476511d330fdc5e8d65e392601ad295a963b3f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,11 +31,18 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'androidx.core:core-ktx:1.8.0' implementation "androidx.recyclerview:recyclerview:1.2.1" implementation 'com.gitee.zhangmengxiong:MXStarter:v1.0.0' implementation 'com.github.bumptech.glide:glide:4.13.2' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' + + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" // rxpermission implementation 'io.reactivex.rxjava3:rxjava:3.0.4' diff --git a/app/src/main/java/com/mx/imagepicker_sample/MainActivity.kt b/app/src/main/java/com/mx/imagepicker_sample/MainActivity.kt index 17eb1952164642ff0e92eab2dc3a03daaaf65554..0fe7aef8976ff8f8531e1d3437d0389ea407d9f4 100644 --- a/app/src/main/java/com/mx/imagepicker_sample/MainActivity.kt +++ b/app/src/main/java/com/mx/imagepicker_sample/MainActivity.kt @@ -4,7 +4,8 @@ import android.Manifest import android.net.Uri import android.os.Bundle import android.view.View -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide import com.gyf.immersionbar.ImmersionBar import com.mx.imgpicker.MXImagePicker @@ -13,10 +14,11 @@ import com.mx.imgpicker.builder.MXCaptureBuilder import com.mx.imgpicker.builder.MXPickerBuilder import com.mx.imgpicker.models.MXPickerType import com.mx.imgpicker.compress.MXImageCompress +import com.mx.imgpicker.utils.MXScanBiz import com.mx.starter.MXStarter import java.io.File -class MainActivity : AppCompatActivity() { +class MainActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -150,5 +152,7 @@ class MainActivity : AppCompatActivity() { ), "图片详情" ) } + MXImagePicker.init(application) +// MXScanBiz.scanAll(this, lifecycleScope) } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4ef87e5276ae8e587d75c7a00cfea97387c6dc52..b484a12f2c363174b406ccbb5481f02f66d24ddd 100644 --- a/build.gradle +++ b/build.gradle @@ -37,5 +37,5 @@ ext { minSdkVersion = 19 versionCode = 1 - versionName = "1.5.7" + versionName = "1.5.8" } \ No newline at end of file