高仿微信朋友圈,點擊查看大圖,放大 縮小,可自定義
前言
相信大家都用過微信,在微信盆友圈中,點點擊一個小圖片的時候,會很自然的切換到大圖模式,然后再點擊一下就縮回去,這個動畫效果非常好看,為了到達這個效果首先確認微信的動畫效果哪些
實現目的
- 點擊小圖 漸變切換到大圖,背景漸變成黑色
- 點擊大圖 漸變到小圖,背景變成原來的樣式
- 往下拖拽,背景的顏色根據往下拖拽的進度改變,越往下 顏色越淺
- 圖片可以放大,縮小
- 可以滑動
- 小圖到大圖的過程中有一個圖片加載進度
目前這個使我們需要完成的需求,eumm 還是有點煩人的
演示效果
最終的項目效果如下圖所示
漸變放大縮小 | 拖動效果 | 放大縮小 |
---|---|---|
![]() |
![]() |
![]() |
支持多種類型的 imagetype (matrix 和 center 不支持)
fix_xy | fit_start | fix_center |
---|---|---|
![]() |
![]() |
![]() |
fix_end | center_crop | center_inside |
---|---|---|
![]() |
![]() |
![]() |
實現講解
滑動分析
一下子讓拿到這個需求,其實還是比較懵逼的,不過飯要一口一口吃,既然這個是仿造微信的,他的最基本的需求就是 點擊小圖 然后顯示一個大圖,并且能夠切換,分析可最基本的需求,一個activity + viewpager 搞定 打完收工,動畫什么的都是浮云~~
滑動 viewpager + activity ,點擊跳轉到新的activity 中
完成了第一個基本的需求
產品說:"不行,要有動畫"
項目中使用的是viewpager2 算嘗個鮮,實際使用中也沒發現問題
點擊小圖切換到大圖的過程 貍貓換代太子
仔細的看一下上面的動畫,一個小圖的activityA 跳轉到 大圖的 activityB ,我想到的一個思路是 當跳轉到activityB 以后,首先在B 的位置上畫一個 和A上一模一樣的imageview 然后 縮放的 大圖的位置
那么問題來了,如何在 activityB 上顯示一個和activityA 上相同的imageview 呢
我想到的辦法是 將 activityA 的ImageView 記錄下來,然后在 activityB 中 將 imageview 的 寬 高 和 圖片的 scaleType 設置的和他相同 這樣圖片的大小 和樣式就和 activityA 中一樣了
如何將 activityB 的圖片位置確定下來
我們一開始得到的 activityA 中的 imageview 我們是知道他的xy 坐標的 只需要將這個坐標給到 activityB 中就行了
代碼如下
override fun beforeTransition(
photoId: Int,
fullView: ImageView,
thumbnailView: ImageView
) {
fullView.scaleType = thumbnailView.scaleType
fullView.layoutParams = fullView.layoutParams.apply {
width = thumbnailView.width
height = thumbnailView.height
val location = getLocationOnScreen(thumbnailView)
when (fullView.parent) {
is ConstraintLayout -> {
val constraintSet = ConstraintSet().apply {
clone(fullView.parent as ConstraintLayout)
clear(photoId, ConstraintSet.START)
clear(photoId, ConstraintSet.TOP)
clear(photoId, ConstraintSet.BOTTOM)
clear(photoId, ConstraintSet.RIGHT)
//重新建立約束
connect(
photoId, ConstraintSet.TOP, ConstraintSet.PARENT_ID,
ConstraintSet.TOP, location[1]
)
connect(
photoId, ConstraintSet.START, ConstraintSet.PARENT_ID,
ConstraintSet.START, location[0]
)
}
constraintSet.applyTo(fullView.parent as ConstraintLayout)
}
else -> {
if (this is ViewGroup.MarginLayoutParams) {
marginStart = location[0]
topMargin = location[1]
}
}
}
}
}
activityB 中的圖片 變大
上面的 beforeTransition 方法 我們已經可以 從 activityA 跳轉到 activityB 并且在activityB 中繪制了一個和activityA 中相同大小,位置,樣式的圖片,接著 我們要把這個圖片變大。。既然是變大,肯定少不了動畫,分析一下這個由小到大,
圖片的寬高不在是原來小圖的 位置也不再是小圖的 ,樣式也不再是小圖的,真正的變成了太子。。
override fun startTransition(fullView: ImageView, thumbnailView: ImageView) {
fullView.scaleType = ImageView.ScaleType.FIT_CENTER
fullView.layoutParams = fullView.layoutParams.apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
height = ViewGroup.LayoutParams.MATCH_PARENT
if (this is ViewGroup.MarginLayoutParams) {
marginStart = 0
topMargin = 0
}
}
}
override fun transitionSet(durationTime: Long): Transition {
return TransitionSet().apply {
addTransition(ChangeBounds())
addTransition(ChangeImageTransform())
duration = durationTime
interpolator = DecelerateInterpolator()
}
}
背景的顏色變成黑色
拿到父類的view 改變他的背景顏色透明度 加上一個 漸變的動畫效果就行了
//修改進入的時候背景 漸變 黑色
fun start(
parent: View,
originalScale: Float,
duration: Long
) {
val valueAnimator = ValueAnimator()
valueAnimator.duration = duration
valueAnimator.setFloatValues(originalScale, 1f)
valueAnimator.addUpdateListener { animation ->
parent.setBackgroundColor(
ColorTool.getColorWithAlpha(Color.BLACK, (animation.animatedValue as Float))
)
}
valueAnimator.start()
}
大圖變成小圖
上面的小圖到大圖的 看官理解啦,那么大圖到小圖 就返回來就行啦
圖片的放大,縮小
這個圖片的放大縮小,我用的是開源的框架 PhotoView,
手指拖動改變大小和背景顏色
先看一下效果
返回原來的狀態 | 變成小圖 |
---|---|
![]() |
![]() |
分析一下 這里有兩個動畫,
- 下拉的位置不大,返回原來的狀態,
- 下拉的位置很大,直接變成小圖
因為 PhotoView 不知道 圖片的 drag 所以 在 PhotoView 的基礎上進行拓展
object ImageDragHelper {
//是否正在移動
private var isDragIng = false
//是否正在動畫
var isAnimIng = false
fun startDrag(
image: PhotoView,
holder: ViewPagerAdapter.PhotoViewHolder,
listener: OnDragAnimListener
) {
image.setOnViewDragListener(object : OnViewDragListener {
override fun onDrag(dx: Float, dy: Float) {
}
override fun onScroll(x: Float, y: Float) {
//這里 需要判斷一下滑動的角度 防止和 viewpager 滑動沖突
if (isDragIng) {
//正在播放動畫 不給滑動
if (isAnimIng) {
return
}
//圖片處于縮放狀態
if (image.scale != 1.0f) {
return
}
drag(image, x, y, listener)
} else {
if (abs(x) > 30 && abs(y) > 60) {
//正在播放動畫 不給滑動
if (isAnimIng) {
return
}
//圖片處于縮放狀態
if (image.scale != 1.0f) {
return
}
// 一開始向上滑動無效的
if (y > 0) {
isDragIng = true
drag(image, x, y, listener)
}
}
}
}
override fun onScrollFinish() {
if (isDragIng)
listener.onEndDrag(image,holder)
}
override fun onScrollStart() {
isDragIng = false
}
})
}
private fun drag(image: PhotoView, x: Float, y: Float, listener: OnDragAnimListener) {
listener.onStartDrag(image, x, y)
}
}
知道了 x y 就好處理了
//圖片的縮放 和位置變化
val fixedOffsetY = y - 0
val fraction = abs(max(-1f, min(1f, fixedOffsetY / image.height)))
val fakeScale = 1 - min(0.4f, fraction)
image.scaleX = fakeScale
image.scaleY = fakeScale
image.translationY = fixedOffsetY * dampingData
image.translationX = x / 2 * dampingData
判斷是縮回去 還是 返回原來的樣子,
override fun onEndDrag(
image: PhotoView,
holder: ViewPagerAdapter.PhotoViewHolder
) {
setViewPagerEnable(true)
setZoomable(image, true)
var fraction = setFraction()
if (fraction > 1f) {
fraction = 1f
} else if (fraction < 0f) {
fraction = 0f
}
if (abs(image.translationY) < image.height * fraction) {
AnimDragHelper.start(
getPhotoViewId(),
setDuration(),
image,
getImageArrayList()[mCurrentIndex],
holder,
enterAnimListener,
afterTransitionListener
)
//背景顏色變化
AnimBgEnterHelper.start(parentView, currentScale, setDuration())
} else {
exitAnim(currentScale, holder)
}
}
自定義的圖片加載器
這里原本想著放到框架內部,又想到 郭嬸在 litepal 中說的 做減法,所以沒有放在內部,而是作為demo 的形式放在了外部, 開發者可以更大程度的自定義樣式
demo 用到的是 glide ,
圖片加載進度
這個功能其實不屬于框架本身的樣式,所以我放在了demno 中 供大家參考,有興趣的小伙伴可以參考 Glide 圖片加載進度
下載
Step 1. Add the JitPack repository to your build file
Add it in your root build.gradle at the end of repositories:
allprojects {
repositories {
...
maven { url 'https://www.jitpack.io' }
}
}
Step 2. Add the dependency
dependencies {
implementation 'com.github.JiangHaiYang01:WeChatPhoto:Tag'
}
當前最新版本
使用
step1 繼承 LargerAct
繼承 LargerAct 類型可自定義 sample 中方式的是string
class SampleAct : LargerAct<String>() {
companion object {
const val IMAGE = "images"
const val INDEX = "index"
}
//添加數據源
override fun getData(): ArrayList<String>? {
return intent.getStringArrayListExtra(IMAGE)
}
//長按事件
override fun onLongClickListener() {
Toast.makeText(this, "長按圖片", Toast.LENGTH_LONG).show()
}
//item 布局
override fun getItemLayout(): Int {
return R.layout.item_def
}
//一定要返回一個 PhotoView 的id 內部處理還是需要用到的
override fun getPhotoViewId(): Int {
return R.id.image
}
//當前是第幾個圖片 index 和 image 一一對應
override fun getIndex(): Int {
return intent.getIntExtra(INDEX, 0)
}
//設置持續時間
override fun setDuration(): Long {
return 2000
}
//默認拖動時候的阻尼系數 [0.0f----1.0f] 越小越難滑動
override fun setDamping(): Float {
return 1.0f
}
//設置下拉的參數 [0.0f----1.0f] 越小越容易退出
override fun setFraction(): Float {
return 0.5f
}
//設置原來的圖片源
override fun getImageArrayList(): ArrayList<ImageView> {
return ImagesHelper.images
}
//處理自己的業務邏輯
override fun itemBindViewHolder(
isLoadFull: Boolean,
itemView: View,
position: Int,
data: String?
) {
if (data == null) {
return
}
//這里用到了自己寫的一個 進度條 可自定義
val progressView = itemView.findViewById<CircleProgressView>(R.id.progress)
val imageView = itemView.findViewById<PhotoView>(R.id.image)
//Glide 加載圖片的進度 具體可參考代碼
ProgressInterceptor.addListener(data, object : ProgressListener {
override fun onProgress(progress: Int) {
progressView.visibility = View.VISIBLE
progressView.progress = progress
}
})
//這里為了演示效果 取消了緩存 正常使用是不需要的
val options = RequestOptions()
if (isLoadFull)
options
.placeholder(imageView.drawable)
.override(imageView.width, imageView.height)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
Glide.with(this)
.load(data)
.apply(options)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
Log.i(TAG, "圖片加載失敗")
progressView.visibility = View.GONE
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: com.bumptech.glide.load.DataSource?,
isFirstResource: Boolean
): Boolean {
Log.i(TAG, "圖片加載成功")
progressView.visibility = View.GONE
return false
}
})
.into(imageView)
}
}
step2 添加theme
<activity
android:name=".larger.SampleAct"
android:screenOrientation="portrait"
android:theme="@style/custom_larger" />
step3 跳轉
private fun startAct(
index: Int,
images: ArrayList<String>
) {
val intent = Intent(this, SampleAct::class.java)
//傳入圖片信息 按需求自定義
intent.putStringArrayListExtra(SampleAct.IMAGE, images)
//傳入當前的 index 用于處理viewpager
intent.putExtra(SampleAct.INDEX, index)
startActivity(intent)
}
更新說明
0.0.2
在0.0.1 版本 實際使用的時候發現一個問題 當圖片是 FIT_XY 或者 CENTER_CROP 等 對圖片裁剪的時候 小圖到大圖的過程中會有問題
- 兼容了 CENTER_CROP 等狀態的處理 (MATRIX 和 CENTER 不兼容)
未處理的問題
在 原本圖片的 style 是 MATRIX 或者 CENTER 的時候,圖片不能很好的從 原來的樣式 漸漸切換到 大圖的樣式,這里的原因我還不知道為啥,知道的的小伙伴可以 說一下
apk 下載體驗
源碼
寫在最后
這邊博客其實之前就寫完了,直接放出效果圖 + 使用方式,我想著其實寫不是關鍵,還是想將自己寫的時候的一些心得分享一下