微信朋友圈大图预览及大图关闭,缩放归位效果实现

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>