微信朋友圈大图预览及大图关闭,缩放归位效果实现
jgkh0615
8年前
<p>微信朋友圈的图片预览和预览关闭时,图片的缩放归位效果很赞,一直都比较喜欢,刚好app要用到,于是打算实现这个效果。一开始也是没有思路的,还好网上已经有类似的demo,于是就结合了网上的资料实现了这个效果。</p> <h2><strong>一、效果图</strong></h2> <p>gif看起来可能不是很流畅,实际运行demo效果还是棒棒的</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/1b7b1e7b054433602693e8d702633efe.gif"></p> <p style="text-align:center">gif5新文件 (1).gif</p> <h2><strong>二、实现思路分析</strong></h2> <p>1.从微信android版看,点击预览大图时,应该是从当前界面将一个隐藏的View进行显示,然后再进行动画处理。这只是一种猜测,当然也无须关注它具体是不是这样实现的,我们要做的就是按照这个方案,实现这个效果即可。</p> <p>2.大图预览的显示动画:仔细观察,你会发现图片是从某一张图片的位置开始进行缩放动画显示的,所以我们需要知道缩略图在屏幕中的位置。</p> <p>3.大图预览关闭动画:现在我们看下gif,首先我们进入的是一个图片列表,当我们点击大图预览,然后再单击关闭预览时,图片会缩放到列表中对应的图片位置。可能出现的情况是,我点击的是列表的第一张图片进行查看大图,在大图滑动到最后一张时进行关闭大图预览界面。那么问题来了,如果列表的图片很多,列表的最后一张图片可能还没有显示到屏幕,这种情况我们是没有办法得到缩略图的位置的。怎么办呢?列表是根据手指的滑动而显示的,所以我们只需要在查看大图时,同时将列表中的缩略图滑动到可见的位置即可。当然这里不是用手指去滑动,因为查看大图时,缩略图列表是不可见的。大图滑动时,我们只需要调用列表控件对应的方法滑动列表即可。</p> <p>4.缩放比例计算:列表中的大图,不可能所有的图片大小、高度都一致,所以为了实现比较好的效果,我们需要去动态计算缩放比例。而在查看大图时,我们在未加载出图片时,我们无法拿到大图的实际大小,直接拿屏幕或者控件的高度去计算缩放比,可能会导致图片缩放变形,所以我们需要在加载图片成功后,拿到bitmap宽高,并缓存用于计算。为了提高加载速度及内存优化使用到了LruCache。关于图片加载优化,可以参考我之前的一篇文章 <a href="/misc/goto?guid=4959715945246019386" rel="nofollow,noindex">图片加载优化,拒绝OOM</a></p> <h2><strong>三、代码</strong></h2> <p><strong>具体实现思路已经分析,废话不多说,直接上代码</strong></p> <p><strong>1、xml布局代码</strong></p> <pre> <code class="language-java"><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></code></pre> <p><strong>2、主要代码(代码太多,只贴主要代码了,详情可查看demo)</strong></p> <p><strong>无论是图片预览和是关闭预览都需要获得缩略图Image的位置坐标,所以需要用到View中的getGlobalVisibleRect方法去测量对应缩略图在屏幕中的位置,以实现缩放归位的动画效果。</strong></p> <pre> <code class="language-java">/** * 添加需要显示的图片 * * @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); } };</code></pre> <p><strong>3、显示与隐藏动画</strong></p> <pre> <code class="language-java">/** * 显示照片查看界面 * * @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(); }</code></pre> <p><strong>4、图片位置以及图片缩放比例计算方法</strong></p> <pre> <code class="language-java">/** * 根据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; }</code></pre> <p><strong>5、图片列表位置滑动处理</strong></p> <p><strong>大图查看控件使用的是ViewPager,所以我把列表滑动处理放在了,ViewPager滑动回调的方法中,项目中使用的是RecyclerView,所以直接调用GridLayoutManager的滑动位置方法即可实现列表滑动</strong></p> <pre> <code class="language-java">/** * 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); } };</code></pre> <p><strong>6、图片加载优化及bitmap宽高获取就直接放在了PagerAdapter中,具体看代码</strong></p> <pre> <code class="language-java">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); } } }</code></pre> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/78d9ad3f0e4c</p> <p> </p>