微信朋友圈大圖預覽及大圖關閉,縮放歸位效果實現
微信朋友圈的圖片預覽和預覽關閉時,圖片的縮放歸位效果很贊,一直都比較喜歡,剛好app要用到,于是打算實現這個效果。一開始也是沒有思路的,還好網上已經有類似的demo,于是就結合了網上的資料實現了這個效果。
一、效果圖
gif看起來可能不是很流暢,實際運行demo效果還是棒棒的
gif5新文件 (1).gif
二、實現思路分析
1.從微信android版看,點擊預覽大圖時,應該是從當前界面將一個隱藏的View進行顯示,然后再進行動畫處理。這只是一種猜測,當然也無須關注它具體是不是這樣實現的,我們要做的就是按照這個方案,實現這個效果即可。
2.大圖預覽的顯示動畫:仔細觀察,你會發現圖片是從某一張圖片的位置開始進行縮放動畫顯示的,所以我們需要知道縮略圖在屏幕中的位置。
3.大圖預覽關閉動畫:現在我們看下gif,首先我們進入的是一個圖片列表,當我們點擊大圖預覽,然后再單擊關閉預覽時,圖片會縮放到列表中對應的圖片位置。可能出現的情況是,我點擊的是列表的第一張圖片進行查看大圖,在大圖滑動到最后一張時進行關閉大圖預覽界面。那么問題來了,如果列表的圖片很多,列表的最后一張圖片可能還沒有顯示到屏幕,這種情況我們是沒有辦法得到縮略圖的位置的。怎么辦呢?列表是根據手指的滑動而顯示的,所以我們只需要在查看大圖時,同時將列表中的縮略圖滑動到可見的位置即可。當然這里不是用手指去滑動,因為查看大圖時,縮略圖列表是不可見的。大圖滑動時,我們只需要調用列表控件對應的方法滑動列表即可。
4.縮放比例計算:列表中的大圖,不可能所有的圖片大小、高度都一致,所以為了實現比較好的效果,我們需要去動態計算縮放比例。而在查看大圖時,我們在未加載出圖片時,我們無法拿到大圖的實際大小,直接拿屏幕或者控件的高度去計算縮放比,可能會導致圖片縮放變形,所以我們需要在加載圖片成功后,拿到bitmap寬高,并緩存用于計算。為了提高加載速度及內存優化使用到了LruCache。關于圖片加載優化,可以參考我之前的一篇文章 圖片加載優化,拒絕OOM
三、代碼
具體實現思路已經分析,廢話不多說,直接上代碼
1、xml布局代碼
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<FrameLayout
android:id="@+id/fly_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:orientation="vertical"
android:visibility="invisible">
<View
android:id="@+id/view_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black" />
<com.leo.example.widght.HackyViewPager
android:id="@+id/vp_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true" />
</FrameLayout>
</FrameLayout>
2、主要代碼(代碼太多,只貼主要代碼了,詳情可查看demo)
無論是圖片預覽和是關閉預覽都需要獲得縮略圖Image的位置坐標,所以需要用到View中的getGlobalVisibleRect方法去測量對應縮略圖在屏幕中的位置,以實現縮放歸位的動畫效果。
/**
* 添加需要顯示的圖片
*
* @param photoData
* @param position
* @param rect
*/
public void showPhotoView(List<ItemSubjectsInfoViewModel> photoData, int
position, Rect rect) {
zoomImagePagerAdapter = new ZoomImagePagerAdapter(this);
zoomImagePagerAdapter.addAll(PhotoZoomViewModel.toViewModel(photoData,
actionHide));
binding.vpPager.setAdapter(zoomImagePagerAdapter);
binding.vpPager.addOnPageChangeListener(changeAdapter);
binding.vpPager.setCurrentItem(mCurrentItem = position);
binding.vpPager.setLocked(zoomImagePagerAdapter.getCount() == 1);
binding.flyLayout.getGlobalVisibleRect(finalBounds, globalOffset);
showPhotoAnimator(rect);
}
/**
* 隱藏圖片預覽界面
*
* @return
*/
private Action1<View> actionHide = new Action1<View>() {
@Override
public void call(View view) {
Rect originBound =
ViewUtils.getViewGlobalRect(shadowListAdapter.getItemView(mCurrentItem));
hidePhotoAnimator(originBound);
}
};
3、顯示與隱藏動畫
/**
* 顯示照片查看界面
*
* @param startBounds
*/
private void showPhotoAnimator(Rect startBounds) {
startBounds.offset(-globalOffset.x, -globalOffset.y);
finalBounds.offset(-globalOffset.x, -globalOffset.y);
ViewHelper.setAlpha(binding.viewBg, 1);
binding.flyLayout.setVisibility(View.VISIBLE);
float ratio = RatioUtils.getShowRatio(startBounds, finalBounds);
calculateLocationRect(startBounds, finalBounds, ratio);
binding.vpPager.setPivotX(0);
binding.vpPager.setPivotY(0);
animator = new AnimatorSet();
animator.play(ObjectAnimator.ofFloat(binding.vpPager, View.X,
startBounds.left, finalBounds.left))
.with(ObjectAnimator.ofFloat(binding.vpPager, View.Y,
startBounds.top, finalBounds.top))
.with(ObjectAnimator.ofFloat(binding.vpPager, View.SCALE_X, ratio,
1f))
.with(ObjectAnimator.ofFloat(binding.vpPager, View.SCALE_Y, ratio,
1f));
animator.setDuration(mDuration);
animator.setInterpolator(interpolator);
animator.addListener(showAnimatorListener);
animator.start();
}
/**
* 隱藏照片查看界面
*
* @param originBound
*/
private void hidePhotoAnimator(Rect originBound) {
//如果展開動畫沒有展示完全就關閉,執行退出動畫
if (animator != null) {
animator.cancel();
}
//將背景設為透明
ViewHelper.setAlpha(binding.viewBg, 0);
//測量View位置,獲取坐標參數
binding.flyLayout.getGlobalVisibleRect(finalBounds, globalOffset);
originBound.offset(-globalOffset.x, -globalOffset.y);
finalBounds.offset(-globalOffset.x, -globalOffset.y);
//將ViewPager的位置移動到屏幕中心
binding.vpPager.setPivotX(0);
binding.vpPager.setPivotY(0);
animator = new AnimatorSet();
float ratio = getHideRatio(originBound,
zoomImagePagerAdapter.getImageSizeInfo(mCurrentItem));
calculateLocationRect(originBound, finalBounds, ratio);
animator.play(ObjectAnimator.ofFloat(binding.vpPager, View.X,
originBound.left))
.with(ObjectAnimator.ofFloat(binding.vpPager, View.Y,
originBound.top))
.with(ObjectAnimator.ofFloat(binding.vpPager, View.SCALE_X, 1f,
ratio))
.with(ObjectAnimator.ofFloat(binding.vpPager, View.SCALE_Y, 1f,
ratio));
animator.setDuration(mDuration);
animator.setInterpolator(interpolator);
animator.addListener(hideAnimatorListener);
animator.start();
}
4、圖片位置以及圖片縮放比例計算方法
/**
* 根據View的寬高,計算動畫及顯示位置的偏移量
*
* @param startBounds
* @param finalBounds
* @param ratio
*/
private void calculateLocationRect(Rect startBounds, Rect finalBounds, float
ratio) {
if ((float) finalBounds.width() / finalBounds.height() > (float)
startBounds.width() / startBounds.height()) {
float startWidth = ratio * finalBounds.width();
float deltaWidth = (startWidth - startBounds.width()) / 2;
startBounds.left -= deltaWidth;
startBounds.right += deltaWidth;
return;
}
float startHeight = ratio * finalBounds.height();
float deltaHeight = (startHeight - startBounds.height()) / 2;
startBounds.top -= deltaHeight;
startBounds.bottom += deltaHeight;
float startWidth = ratio * finalBounds.width();
float deltaWidth = (startWidth - startBounds.width()) / 2;
//根據圖片所在位置計算位置偏移量
if (mRow > 1 && LocationUtils.isRight(mCurrentItem, mRow)) {
startBounds.left -= startWidth - startBounds.width();
} else if (!LocationUtils.isRight(mCurrentItem, mRow) && !
LocationUtils.isLeft(mCurrentItem, mRow) || 3 == 1) {
startBounds.left -= deltaWidth;
startBounds.right += deltaWidth;
}
}
/**
* 獲取隱藏縮放比
*
* @param info
* @param endBounds
*/
private float getHideRatio(Rect endBounds, ImageSizeInfo info) {
float ratio;
//可能出現圖片未加載完畢,拿不到圖片寬高數據的情況,此時默認拿外層ViewGroup的寬高,保證基本
的效果即可
if (info == null) {
info = new ImageSizeInfo();
}
if (info.getWidth() == 0 || info.getHeight() == 0) {
info.setWidth(binding.flyLayout.getWidth());
info.setHeight(binding.flyLayout.getHeight());
}
//根據圖片顯示的尺寸計算,隱藏時的縮放比例
//為保證縮放效果,以圖片寬、高中,以參數小的為計算基準
float scale = (float) screenWidth / info.getWidth();
if (info.getWidth() > info.getHeight()) {
ratio = RatioUtils.getHeightRatio(info, endBounds, scale);
//如果計算的縮放的最終寬度,要小于RecyclerView中item寬度
//則重新以圖片高度為基準,進行計算
if (ratio * info.getWidth() * scale < endBounds.width()) {
ratio = RatioUtils.getWidthRatio(info, endBounds, scale);
}
return ratio;
}
ratio = RatioUtils.getWidthRatio(info, endBounds, scale);
//如果計算的縮放的最終高度,要小于RecyclerView中item高度
//則重新以圖片寬度為基準,進行計算
if (ratio * info.getHeight() * scale < endBounds.height()) {
ratio = RatioUtils.getHeightRatio(info, endBounds, scale);
}
return ratio;
}
5、圖片列表位置滑動處理
大圖查看控件使用的是ViewPager,所以我把列表滑動處理放在了,ViewPager滑動回調的方法中,項目中使用的是RecyclerView,所以直接調用GridLayoutManager的滑動位置方法即可實現列表滑動
/**
* ViewPage滑動監聽
*/
private ViewPager.OnPageChangeListener changeAdapter = new
ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
if (position >= shadowListAdapter.size() || position == mCurrentItem) {
return;
}
mCurrentItem = position;
//因實現圖片歸位的動畫效果,需要先測量View在屏幕中的坐標,所以需要保證RecyclerView
中的item必須顯示在屏幕中
//所以圖片預覽切換圖片時,需要同時滑動RecyclerView
GridLayoutManager layoutManager = (GridLayoutManager)
binding.rvView.getLayoutManager();
layoutManager.scrollToPositionWithOffset(position, 0);
}
};
6、圖片加載優化及bitmap寬高獲取就直接放在了PagerAdapter中,具體看代碼
public class ZoomImagePagerAdapter extends PagerViewModelAdapter<ItemShowPhotoImageBinding, PhotoZoomViewModel> {
private SparseArray<ImageSizeInfo> infoSparseArray = new SparseArray<>();
private SparseArray<PhotoView> viewSparseArray = new SparseArray<>();
public ZoomImagePagerAdapter(Context context) {
super(context);
}
@Override
public void onBindDataToView(BaseDataViewHodler<ItemShowPhotoImageBinding>
holder, int position, PhotoZoomViewModel photoViewModel) {
String url = photoViewModel.getImageUrl().get();
Bitmap bitmap = LruCacheUtils.getInstance().getBitmapFromMemCache(url);
PhotoView photoView = holder.getBinding().showPhoto;
viewSparseArray.put(position, photoView);
if (bitmap != null) {
cacheImageData(bitmap, url, position);
photoView.setImageBitmap(bitmap);
} else {
PhotoLoader.displayImageLruCaches(photoView, url, getTarget(photoView,
url, position));
}
}
@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
public ImageSizeInfo getImageSizeInfo(int position) {
return infoSparseArray.get(position);
}
/**
* 將View對象置為null
*/
public void onDestoryPhotoView() {
for (int i = 0; i < viewSparseArray.size(); i++) {
PhotoView view = viewSparseArray.get(i);
if (view != null) {
view = null;
}
}
infoSparseArray.clear();
viewSparseArray.clear();
}
/**
* 獲取BitmapImageViewTarget
*/
private BitmapImageViewTarget getTarget(ImageView imageView, final String url,
final int position) {
return new BitmapImageViewTarget(imageView) {
@Override
protected void setResource(Bitmap resource) {
super.setResource(resource);
cacheImageData(resource, url, position);
}
};
}
/**
* 緩存圖片數據
*/
private void cacheImageData(Bitmap resource, String url, int position) {
ImageSizeInfo info = infoSparseArray.get(position);
if (info == null) {
info = new ImageSizeInfo();
}
if (resource != null) {
info.setWidth(resource.getWidth());
info.setHeight(resource.getHeight());
infoSparseArray.put(position, info);
LruCacheUtils.getInstance().addBitmapToMemoryCache(url, resource);
}
}
}
來自:http://www.jianshu.com/p/78d9ad3f0e4c