先看看效果:
前言
由于相册查询会使一个持续的过程,所以基于协程+ViewModel 开发的话,会省去很多生命周期的管理,防止内存泄漏和一些引用长持的问题。而Flow 替换RxJava 是由于Flow 的背压模式强于RxJava,也更方便简洁,而且可以对上流和下流管道切换不同的线程。
整个开发逻辑包含以下功能部分
- 1、列表查询
- 2、标题列表查询
- 3、大图预览
- 4、大图下拉关闭图片返回列表位置
一、列表查询
/**
* 加载列表数据
*
* @param context
* @param selectionArgsName
* @param loadType
* @param id
*/
@OptIn(DelicateCoroutinesApi::class)
@SuppressLint("Range")
private fun loadListData(
context: Context?,
selectionArgsName: String?,
loadType: Int,
id: String
) {
if (selectionArgsName != null && selectionArgsName != this.selectionArgsName) {
albumLoaderBuilder.callBack?.clearData()
allData.clear()
isRunning = true
}
this.selectionArgsName = selectionArgsName
this.context = context
this.loadType = loadType
this.id = id
album.clear()
latelyList.clear()
allList.clear()
loaderStatus = LoaderStatus.LOADING
launch = GlobalScope.launch(Dispatchers.IO) {
val mCursor = getCursor(selectionArgsName, id, context, loadType)
var size = 0
var parentFile: File? = null
if (mCursor != null) {
count = mCursor.count
val size1 = allData.size - 1
mCursor.moveToPosition(size1)
while (mCursor.moveToNext() && isRunning && size < albumLoaderBuilder.pageSize) {
val path =
mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media.DATA))
if (!isImageFile(path)) {
continue
}
val file = File(path)
//如果默认是相机,过滤掉别的图片,获取相机图片
val displayName =
mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME))
parentFile = File(path).parentFile // 获取该图片的父路径名
var width =
mCursor.getInt(mCursor.getColumnIndex(MediaStore.Images.Media.WIDTH))
var height =
mCursor.getInt(mCursor.getColumnIndex(MediaStore.Images.Media.HEIGHT))
val rotation = Utils.readPictureDegree(path)
if (rotation == 90 || rotation == 270) {
width =
mCursor.getInt(mCursor.getColumnIndex(MediaStore.Images.Media.HEIGHT))
height =
mCursor.getInt(mCursor.getColumnIndex(MediaStore.Images.Media.WIDTH))
}
val galleryInfoEntity = GalleryInfoEntity()
.setImgName(displayName)
.setImgPath(path)
.setImgWidth(width)
.setImgHeight(height)
.setDisplayName(selectionArgsName)
.setDisplayId(id)
var pdf = 0 //判断此图片所属的相册是否已存在,pdf为0表示图片所属的相册还不存在
for (i in album.indices) {
if (album[i].dirPath.equals(parentFile.absolutePath, ignoreCase = true)) {
pdf = 1
album[i].galleryInfo
album[i].galleryInfo?.add(galleryInfoEntity)
}
}
if (pdf == 0) {
val list = CopyOnWriteArrayList<GalleryInfoEntity>()
list.add(galleryInfoEntity)
album.add(
AlbumInfoEntity()
.setAlbumName(parentFile.name)
.setDirPath(parentFile.absolutePath)
.setGalleryInfo(list)
)
}
if (albumLoaderBuilder.isShowLastModified) {
}
if (file.lastModified() >= Utils.stateTime) {
latelyList.add(galleryInfoEntity)
}
allList.add(galleryInfoEntity)
size++
}
if (parentFile != null) {
album.add(
0, AlbumInfoEntity()
.setAlbumName("全部图片")
.setDirPath(parentFile.absolutePath)
.setGalleryInfo(allList)
.setAll(true)
)
if (latelyList.size > 0) {
album.add(
AlbumInfoEntity()
.setAlbumName("最近照片")
.setDirPath(parentFile.absolutePath)
.setGalleryInfo(latelyList)
.setAll(true)
)
}
}
withContext(Dispatchers.Main) {
setData(
allList
)
}
}
}
}
二、标题列表查询
job = GlobalScope.launch(Dispatchers.Main) {
flow {
emit(data)
}.map {
scanAllAlbumData(it)
}.flowOn(Dispatchers.IO).collect {
Log.i("THREAD", "" + (Thread.currentThread() == Looper.getMainLooper().thread))
mAlbumDataReceiver?.onAlbumDataObserve(it)
}
}
private fun scanAllAlbumData(cursor: Cursor): List<AlbumData?> {
val albumDataList: MutableList<AlbumData?> = ArrayList()
while (isCursorEnable(cursor) && cursor.moveToNext()) {
val albumData = AlbumData.valueOf(cursor, mContext)
albumDataList.add(albumData)
}
return albumDataList
}
三、大图预览
主要使用的是viewpager2 去做的
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager_horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:background="@color/color_010101"
android:orientation="horizontal"
app:bindPageListener="@{model.pagerListener}"
app:bindViewPagerAdapter="@{model.previewAdapter}"
tools:ignore="MissingConstraints" />
try {
if (DataBindingUtil.getBinding<ItemPreviewLayoutBinding?>(holder.itemView) == null) {
ItemPreviewLayoutBinding.bind(holder.itemView)
}
val binding = holder.getBinding<ItemPreviewLayoutBinding>()
binding?.model = model
binding?.photoView?.scaleType=ImageView.ScaleType.FIT_XY
binding?.photoView?.let {
Glide.with(context)
.asBitmap()
.load(item.imgPath)
.dontAnimate()
.fitCenter()
.placeholder(binding.photoView.drawable)
.into(it)
}
Log.e("TAG","IMG PATH"+item.imgPath)
} catch (e: Exception) {
}
四、大图下拉关闭图片返回列表位置**
1、监听滑动后,每个列表的高度,计算出Rect
inner class PageListener : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val data = previewAdapter?.get()?.data
currentPosition = position
data?.let {
val galleryInfoEntity = data[position]
previewCheckStatus?.set(galleryInfoEntity.isSelected)
val data1 = adapter?.get()?.data
data1?.let {
val indexOf = it.indexOf(galleryInfoEntity)
Log.i(TAG, "the index of=$indexOf")
if (indexOf != -1 && data.size > indexOf) {
val viewByPosition = adapter?.get()?.getViewByPosition(indexOf, R.id.img)
viewByPosition?.getGlobalVisibleRect(rect)
viewByPosition?.height?.let {
it1->
itemHeight=it1
}
//设置最小缩放
}
}
}
}
}
2、拖动松手后,计算当前图片的中心的X\Y 以及图片列表小图片的中心点X\Y
private fun performExitAnimation(
view: DragPhotoView,
x: Float,
y: Float,
w: Float,
h: Float,
scale: Float
) {
view.finishAnimationCallBack()
val array = IntArray(2) {
0 }
val mOriginLeft = rect.left
val mOriginTop = rect.top
val mOriginHeight = rect.height()
val mOriginWidth = rect.width()
val mOriginCenterX = mOriginLeft + mOriginWidth / 2
val mOriginCenterY = mOriginTop + mOriginHeight / 2
Log.i(TAG, "mOriginTop=$mOriginTop")
val currentWidth = w * scale
val currentHeight = h * scale
view.getLocationInWindow(array)
val viewX: Float = w / 2 + x - currentWidth / 2 + array[0]
val viewY: Float = h / 2 + y - currentHeight / 2 + array[1]
view.x = viewX
view.y = viewY
view.setOutLine(itemHeight,mOriginWidth)
val centerX = view.x + mOriginWidth / 2
val centerY = view.y + mOriginHeight / 2
val translateX = mOriginCenterX - centerX
val translateY = mOriginCenterY - centerY
val animatorSet = AnimatorSet()
animatorSet.duration = 400
animatorSet.interpolator = DecelerateInterpolator()
val translateXAnimator: ValueAnimator = ValueAnimator.ofFloat(view.x, view.x + translateX)
translateXAnimator.addUpdateListener {
valueAnimator ->
view.x = (valueAnimator.animatedValue as Float)
}
val translateYAnimator: ValueAnimator =
ValueAnimator.ofFloat(view.y, view.y + translateY)
translateYAnimator.addUpdateListener {
valueAnimator ->
view.y = (valueAnimator.animatedValue as Float)
}
animatorSet.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animator: Animator?) {
}
override fun onAnimationEnd(animator: Animator) {
animator.removeAllListeners()
leftFinish()
}
override fun onAnimationCancel(animator: Animator?) {
}
override fun onAnimationRepeat(animator: Animator?) {
}
})
animatorSet.playTogether(translateXAnimator, translateYAnimator)
animatorSet.start()
}
上诉都是贴纸关键核心代码,具体逻辑可以去fork github 代码看看。