项目地址: (能弱弱的求个star或者fork么QAQ)
-
上篇链接:
-
下篇链接:
【Warning】:
本篇完整的从思考->寻找->编写代码->最终完成
来阐述我如何实现本篇预览图功能
本篇篇幅较长,请带上一定的耐心
本篇图片较多,流量党请注意
本篇比较抽象,我会尽量形象的阐述
本篇预览图:
前言
正如上一篇文章所说,一个app动人之处在于细节的研磨和富有动感的交互。
在微信的朋友圈,我们点击图片可以感觉到像预览图那样的效果:点击某张图片,然后它会放大到全屏,再点击,则会缩小到原来的那个地方
这种交互看起来非常赞,最起码看得顺眼。
然而很多时候交互动作设计的时候看起来确实很棒,但对于我等程序员来说,设计棒,设计酷往往会让我们摆出一张苦逼脸
—— 臣妾做不到啊,陛下。
但迫于Money的压力下,我们往往不得不硬着头皮上。
正如今天这个效果,确实一开始是没有任何头绪,在思考实现的过程中,我曾经想过如下几种方法:
- 直接重写一个ImageView,利用martix来放大图片
- 点击的时候,通过windowmanager动态添加一个imageview,然后让这个imageview实现动画
- 弄两个view,一个隐藏的,一个是listview中的,然后点击的时候把隐藏的显示,并进行动画。
- 甚至想过,直接上activityoptions....使用activity的转场动画
但实际上,以上的方法貌似都可以,但实际上真要我去干了,就犹豫了,且不说运行效率,但起码可以预测到代码量。。。
然而,一次神奇的发现,让我解决了这个问题,准确的说,是谷歌早就解决了这个问题。
羽翼君探索篇
发现
对于面向搜索引擎编程的我们,其实一直都习惯于有问题找度娘,或者找谷歌。
鉴于度娘找到的技术文章基本都是你抄我,我抄你,于是我只好到谷歌以**"android scale a view to full screen"**来找答案,奈何找来找去都是关于如何让imageview的图片填充整个屏幕的。
于是换个思路,除了scale,我们不是经常还能接触到"zoom"这个关键词么,于是就继续谷歌**"android zoom a view to full screen"**
结果第一个结果就是Android开发者文档的train项目:
点进去一看,瞬间满满的幸福感,原来头疼了好久的问题,人家谷歌早就给出了答案
而且,不得不说的是,这个项目仅仅是在Android的培训项目,相当于打游戏第一关的新手教程那样吧,具体地址可以点这里(http://developer.android.com/intl/zh-cn/training/index.html )
事实上,在完成了这篇文章的效果后,我到官方培训这里看了几次,于是决定,我必须要把这里所有东西弄明白。
这里真的要给谷歌一万个赞。
难点
在得到官方培训这个超级大外挂后,最难的地方其实已经没有什么障碍了,剩下的就是该如何适配到我们的项目中。
从我们日常使用朋友圈的经验看,关于图片点击放大会涉及到这么几个难点:
-
朋友圈的图片是1~9张,那么该如何确保ViewPager可以加载相等数量的图片
-
点击图片是否应该跳转到新的Activity
-
假如我点击第一张图片,在ViewPager滑倒第三张图片,那么点击图片退出时该如何确保View缩小后的位置与第三张图片一致而非缩小到第一张图片的位置。
-
ViewPager浏览的时候图片放大和缩小如何实现
在官方的demo中,仅仅只有一张图片的浏览,也就是说仅仅是展示了一张图片缩放到全屏的方法,所以我们只有去完全的理解demo,才能继续我们的工程。
不过在真正实现之前,上面的问题我们其实可以回答几个:
-
针对第一个问题,我们可以在点击图片的时候,把当前图片所在的Item的图片地址数组传到adapter里面然后通知更新
-
在我们使用朋友圈的时候,可以感觉点击图片放大这个过程非常的快,而如果重新打开一个Activity,则需要经过那么多的onCreate等生命期方法,那么肯定不会有这么灵敏的反应,所以很明显,这个ViewPager其实是包含在朋友圈所在的窗口,只是平时隐藏起来而已。
-
针对第三个问题在稍后的阐述中回答。
-
第四个问题,我觉得想都不用想,直接上PhotoView这个库。
原理篇
官方Demo详解
事实上,官方的demo中的注释是十分的清楚的,官方的Demo最主要依靠的是两个东西:
- getGlobalVisibleRect
- ObjectAnimator
我们都知道,一个View是可以通过getLocationOnScreen或者**getLocationInWindow **得到相对于整个屏幕/相对于父控件的xy位置信息。
而getGlobalVisibleRect/getLocalVisibleRect跟上面的这个其实差不多,不过不同的是,它得到的不是xy位置信息,而是得到指定View在屏幕中展示的矩形信息
简单的描述就是前者得到view的原点信息,后者得到view的2D形状信息。
官方的demo则是通过这个得到两个view的Rect:
- 点击的view的rect(startRect)
- 最终放大后的view的rect(endRect)
得到两个view的矩形后,就可以得到双方的缩放比。
通过这个比例,可以做的事情就很多了,官方的demo则是通过这个比例,来计算出以下的参数:
- 小图的x位置和大图的x位置,得到放大后View水平方向上应该偏移多少。
- 因为仅仅是view的缩放是不够的,因为需要保证放大后的View处于屏幕中央,而不是说偏左偏右。
- 同理得到垂直方向的偏移。
- 得到水平方向的缩放比,也就是上面所说的缩放比。
- 同理得到垂直方向的缩放比。
得到这些参数后,就通过ObjectAnimator,操作的对象是隐藏在屏幕中的最终展示的View,通过监听它的数值变化,从而不断的更新展示的View的属性,给人造成原来的view放大的错觉。
或许文字说的有点枯燥,所以,直接上AE,弄出一个动图,相信大家一看就明白:
在图层的结构上如下动图:
在点击的之后,会发生如下动作:
总结起来就是:
- 点击时,得到被点击的view的rect,和最终效果的rect
- 通过两个rect计算缩放比率
- 动画开始之前将最终展示的view设置为可见
- 由于ObjectAnimator开始的值是被点击的view的rect值,所以最终展示的view会从被点击的view的大小开始播放
- 随着动画进行,不断的改变自己的x,y,scaleX,scaleY
- 从而达到让人感觉view从小变大的错觉
代码篇
Step 1- 布局/MVP的方法添加
呼呼,又是AE,又是穹妹(手动斜眼)的,终于可以开始弄我们的代码了。
从上面我们知道,要实现这种伪放大效果,最重要的是得到开始和结束两个view的rect,而我们由于是使用viewpager,所以我们穿值传递的是一个数组,这个数组就是当前Item所拥有的imageview的rect数组。
因此回到我们的Activity,由于我们采用MVP模式,所以在View层增加一个方法:
public interface DynamicView {...之前的方法不变 // 浏览图片 void showPhoto(@NonNull ArrayListphotoAddress, @NonNull ArrayList originViewBounds, int curSelectedPos);}复制代码
同样在P层也增加这个方法,这里就不贴上来了。
接下啦到我们朋友圈的布局中,添加一个viewpager。
复制代码
值得注意的是,因为微信朋友圈的大图浏览是有背景(黑色)的,所以我们外层用一个布局包裹。
另外由于我们需要使用PhotoView,所以我们的ViewPager将会使用PhotoView作者给出的解决方法:
HackyViewPager代码如下(因为有LICENSE,所以就完整贴出):
import android.content.Context;import android.support.v4.view.ViewPager;import android.util.AttributeSet;import android.view.MotionEvent;/** * Found at http://stackoverflow.com/questions/7814017/is-it-possible-to-disable-scrolling-on-a-viewpager. * Convenient way to temporarily disable ViewPager navigation while interacting with ImageView. * * Julia Zudikova *//** * Hacky fix for Issue #4 and * http://code.google.com/p/android/issues/detail?id=18990 * * ScaleGestureDetector seems to mess up the touch events, which means that * ViewGroups which make use of onInterceptTouchEvent throw a lot of * IllegalArgumentException: pointerIndex out of range. * * There's not much I can do in my code for now, but we can mask the result by * just catching the problem and ignoring it. * * @author Chris Banes */public class HackyViewPager extends ViewPager { private boolean isLocked; public HackyViewPager(Context context) { super(context); isLocked = false; } public HackyViewPager(Context context, AttributeSet attrs) { super(context, attrs); isLocked = false; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (!isLocked) { try { return super.onInterceptTouchEvent(ev); } catch (IllegalArgumentException e) { e.printStackTrace(); return false; } } return false; } @Override public boolean onTouchEvent(MotionEvent event) { return !isLocked && super.onTouchEvent(event); } public void toggleLock() { isLocked = !isLocked; } public void setLocked(boolean isLocked) { this.isLocked = isLocked; } public boolean isLocked() { return isLocked; }}复制代码
在布局弄好后,我们将它include到我们的朋友圈activity,于是目前的层次如下:
我们的viewpager在listview的上方
Step 2 - ViewPager的adapter
adapter很明显,就是为了实现我们的所有方法的,在adapter的设计中,我们需要知道几个地方:
-
ViewPager的adapter如果直接调用adapter.notifydatasetchanged是未必能刷新的,这个跟getItemPosition方法有关,所以如果想adapter刷新,就需要覆写这个。
- 本例的adapter刷新不使用notifydatasetchanged,而是直接setAdapter,viewpager的setAdapter方法会触发destroyItem,所以我们直接使用setAdapter
-
adapter中,我们只管视图的渲染,不管事件的处理,事件的处理我们通过接口抛到外部处理。
因此我们的adapter将会这么设计:
/** * Created by 大灯泡 on 2016/4/12. * 图片浏览窗口的adapter */public class PhotoBoswerPagerAdapter extends PagerAdapter { private static final String TAG = "PhotoBoswerPagerAdapter"; //=============================================================datas private ArrayListphotoAddress; private ArrayList originViewBounds; //=============================================================bounds private Context mContext; private LayoutInflater mLayoutInflater; public PhotoBoswerPagerAdapter(Context context) { mContext = context; mLayoutInflater = LayoutInflater.from(context); photoAddress = new ArrayList<>(); originViewBounds = new ArrayList<>(); } public void resetDatas(@NonNull ArrayList newAddress, @NonNull ArrayList newOriginViewBounds) throws IllegalArgumentException { if (newAddress.size() != newOriginViewBounds.size() || newAddress.size() <= 0 || newOriginViewBounds.size() <= 0) { throw new IllegalArgumentException("图片地址和图片的位置缓存不对等或某一个为空"); } photoAddress.clear(); originViewBounds.clear(); photoAddress.addAll(newAddress); originViewBounds.addAll(newOriginViewBounds); } @Override public int getCount() { return photoAddress.size(); } @Override public Object instantiateItem(ViewGroup container, int position) { return null; } int[] pos = new int[1]; @Override public void setPrimaryItem(ViewGroup container, int position, Object object) { super.setPrimaryItem(container, position, object); } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } //=============================================================点击消失的interface private OnPhotoViewClickListener mOnPhotoViewClickListener; public OnPhotoViewClickListener getOnPhotoViewClickListener() { return mOnPhotoViewClickListener; } public void setOnPhotoViewClickListener(OnPhotoViewClickListener onPhotoViewClickListener) { mOnPhotoViewClickListener = onPhotoViewClickListener; } public interface OnPhotoViewClickListener { void onPhotoViewClick(View view, Rect originBound, int curPos); }}复制代码
在adapter中,我们存放着这些参数:
- photoAddress:存放图片地址的数组
- originViewBounds:点击时所处item的所有imageview的rect
然后还有我们内部定义的接口:OnPhotoViewClickListener,这个接口在点击Viewpager里面的PhotoView时会触发。
Step 3 - PhotoPagerManager
在adapter初步结构设计后,我们暂时先不管,接下来我们需要处理的就是缩放动画和点击的事件处理。
由于我们的Activity作为MVP的View,代码量已经比较多了,所以我们将动画的实现和点击事件的处理封装到另一个类里,委托它进行操作。
在设计这个类之前,我们需要确定一下需要的委托管理的东西:
- adapter:负责处理adapter内部PhotoView点击时回调的动作
- pager:需要pager的setAdapter来进行刷新以及setCurrentItem来定位到我们点击的图片位于图片序列的位置
- container:就是布局里包裹着ViewPager的RelativeLayout,我们通过它做一个点击消逝时的透明度渐变动画,同时endRect也是依靠它来得到
由此,我们初步设计以下结构:
/** * Created by 大灯泡 on 2016/4/12. * 相册展示的管理类 */public class PhotoPagerManager implements PhotoBoswerPagerAdapter.OnPhotoViewClickListener { private Context mContext; private PhotoBoswerPagerAdapter adapter; private HackyViewPager pager; private Rect finalBounds; private Point globalOffset; private View container; //私有构造器 private PhotoPagerManager(Context context, HackyViewPager pager, View container) { if (container != null) { finalBounds = new Rect(); globalOffset = new Point(); this.mContext = context; this.container = container; this.pager = pager; adapter = new PhotoBoswerPagerAdapter(context); adapter.setOnPhotoViewClickListener(this); } else { throw new IllegalArgumentException("PhotoPagerManager >>> container不能为空哦"); } } //静态工厂 public static PhotoPagerManager create(Context context, HackyViewPager pager, View container) { return new PhotoPagerManager(context, pager, container); } //共有调用方法,传入图片地址和view的可见矩形数组 public void showPhoto( @NonNull ArrayListphotoAddress, @NonNull ArrayList originViewBounds, int curSelectedPos) { } //当前正在进行的动画,如果动画没展示完,就将其取消以执行下一个动画 private AnimatorSet curAnimator; //私有showPhoto处理 private void showPhotoPager(@NonNull ArrayList originViewBounds, int curSelectedPos) { } //pager的PhotoView点击回调,用于执行消失时的缩小动画 @Override public void onPhotoViewClick(View view, Rect originBound, int curPos) { } //计算缩放比率 private float calculateRatio(Rect startBounds, Rect finalBounds) { } //销毁 public void destroy() { adapter.destroy(); mContext = null; adapter = null; pager = null; finalBounds = null; globalOffset = null; container = null; }}复制代码
可以看得出,我们的重头戏全在showPhoto里面
在私有构造器里面我们将需要的成员进行赋值,同时adapter需要实现我们在第二步定义的接口。
接下来我们补充共有的showPhoto方法:
public void showPhoto( @NonNull ArrayListphotoAddress, @NonNull ArrayList originViewBounds, int curSelectedPos) { adapter.resetDatas(photoAddress, originViewBounds); pager.setAdapter(adapter); pager.setCurrentItem(curSelectedPos); pager.setLocked(photoAddress.size() == 1); container.getGlobalVisibleRect(finalBounds, globalOffset); showPhotoPager(originViewBounds, curSelectedPos); }复制代码
每次调用show方法我们都需要刷新adapter的数据,然后使用setAdapter来进行刷新。
接下来判断传进来的图片是否只有一张,如果只有一张,就不允许viewpager滑动,setLocked方法是PhotoView作者给出的解决方案带有的,其原理是在Viewpager的onInterceptTouchEvent里通过locked来决定是否拦截事件。
container.getGlobalVisibleRect(finalBounds, globalOffset);
这个在上面的解释里已经有,这里只是直接copy官方demo代码而已。
最后调用私有方法:showPhotoPager
private void showPhotoPager(@NonNull ArrayListoriginViewBounds, int curSelectedPos) { Rect startBounds = originViewBounds.get(curSelectedPos); startBounds.offset(-globalOffset.x, -globalOffset.y); finalBounds.offset(-globalOffset.x, -globalOffset.y); float ratio = calculateRatio(startBounds, finalBounds); pager.setPivotX(0); pager.setPivotY(0); container.setVisibility(View.VISIBLE); container.setAlpha(1.0f); final AnimatorSet set = new AnimatorSet(); set.play(ObjectAnimator.ofFloat(pager, View.X, startBounds.left, finalBounds.left)) .with(ObjectAnimator.ofFloat(pager, View.Y, startBounds.top, finalBounds.top)) .with(ObjectAnimator.ofFloat(pager, View.SCALE_X, ratio, 1f)) .with(ObjectAnimator.ofFloat(pager, View.SCALE_Y, ratio, 1f)); set.setDuration(300); set.setInterpolator(new DecelerateInterpolator()); set.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { curAnimator = set; } @Override public void onAnimationEnd(Animator animation) { curAnimator = null; } @Override public void onAnimationCancel(Animator animation) { curAnimator = null; } @Override public void onAnimationRepeat(Animator animation) { } }); set.start(); }复制代码
这里跟官方的代码基本一致,因为官方代码有注释,所以这里就不详细阐述了。
不过值得留意的是,在动画执行之前必须要将container的alpha设回1,因为我们在退出动画里将它设置为0的。
同理,在PhotoView点击回调里,我们也写出差不多的代码:
@Override public void onPhotoViewClick(View view, Rect originBound, int curPos) { //如果展开动画没有展示完全就关闭,那么就停止展开动画进而执行退出动画 if (curAnimator != null) { curAnimator.cancel(); } container.getGlobalVisibleRect(finalBounds, globalOffset); originBound.offset(-globalOffset.x, -globalOffset.y); finalBounds.offset(-globalOffset.x, -globalOffset.y); float ratio = calculateRatio(originBound, finalBounds); pager.setPivotX(0); pager.setPivotY(0); final AnimatorSet set = new AnimatorSet(); set.play(ObjectAnimator.ofFloat(pager, View.X, originBound.left)) .with(ObjectAnimator.ofFloat(pager, View.Y, originBound.top)) .with(ObjectAnimator.ofFloat(pager, View.SCALE_X, 1f, ratio)) .with(ObjectAnimator.ofFloat(pager, View.SCALE_Y, 1f, ratio)) .with(ObjectAnimator.ofFloat(container, View.ALPHA, 1.0f, 0.0f)); set.setDuration(300); set.setInterpolator(new DecelerateInterpolator()); set.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { curAnimator = set; } @Override public void onAnimationEnd(Animator animation) { curAnimator = null; container.clearAnimation(); container.setVisibility(View.INVISIBLE); } @Override public void onAnimationCancel(Animator animation) { curAnimator = null; container.clearAnimation(); container.setVisibility(View.INVISIBLE); } @Override public void onAnimationRepeat(Animator animation) { } }); set.start(); }复制代码
在退出的动画里,我们需要将SCALE_X和SCALE_Y的动画起始值和目标值替换
- 在放大动画里,我们是从小->大,即计算出来的比率->1f
- 在缩小动画则相反,从大到小
最后补全,哦,不,是copy官方的计算比率的方法:
private float calculateRatio(Rect startBounds, Rect finalBounds) { float ratio; if ((float) finalBounds.width() / finalBounds.height() > (float) startBounds.width() / startBounds.height()) { // Extend start bounds horizontally ratio = (float) startBounds.height() / finalBounds.height(); float startWidth = ratio * finalBounds.width(); float deltaWidth = (startWidth - startBounds.width()) / 2; startBounds.left -= deltaWidth; startBounds.right += deltaWidth; } else { // Extend start bounds vertically ratio = (float) startBounds.width() / finalBounds.width(); float startHeight = ratio * finalBounds.height(); float deltaHeight = (startHeight - startBounds.height()) / 2; startBounds.top -= deltaHeight; startBounds.bottom += deltaHeight; } return ratio; }复制代码
官方的计算方法是这样的:
- 比较最终view的宽高比和起始view的宽高比
- 无论是那种,都需要计算出差值,这个差值用来定位最终view动画播放时的起始位置,让其保证跟起始view一致
在这个类完成后,我们在Activity里仅仅需要两句话调用:
/** * Created by 大灯泡 on 2016/2/25. * 朋友圈demo窗口 */public class FriendCircleDemoActivity extends FriendCircleBaseActivity implements DynamicView, View.OnClickListener, OnSoftKeyboardChangeListener { ... 成员变量略 //图片浏览的pager manager private PhotoPagerManager mPhotoPagerManager; @Override protected void onCreate(Bundle savedInstanceState) { ...略 initView(); ...略 } private void initView() { ...各种findViewById略 //初始化我们的manager mPhotoPagerManager = PhotoPagerManager.create(this, (HackyViewPager) findViewById(R.id.photo_pager), findViewById(R.id.photo_container)); } ...其他方法略 @Override public void showPhoto( @NonNull ArrayListphotoAddress, @NonNull ArrayList originViewBounds, int curSelectedPos) { //事件委托给manager mPhotoPagerManager.showPhoto(photoAddress, originViewBounds, curSelectedPos); }}复制代码
Step 4 - adapter代码补全
实现完manager后,我们就补全我们的adapter代码
在adapter里面,我们主要关注两个方法:
- instantiateItem:初始化view的时候回调
- setPrimaryItem:滑动时回调当前展示着的view
其他方法都是常规方法,就不展示了
初始化的时候,我们的代码非常简单,new一个,add,完。。。
@Override public Object instantiateItem(ViewGroup container, int position) { PhotoView photoView=new PhotoView(mContext); Glide.with(mContext).load(photoAddress.get(position)).into(photoView); container.addView(photoView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); return photoView; }复制代码
在setPrimaryItem中,我们为photoView设置回调:
int currentPos; @Override public void setPrimaryItem(ViewGroup container, int position, Object object) { super.setPrimaryItem(container, position, object); currentPos=position; if (object instanceof PhotoView) { PhotoView photoView = (PhotoView) object; if (photoView.getOnViewTapListener() == null) { photoView.setOnViewTapListener(new PhotoViewAttacher.OnViewTapListener() { @Override public void onViewTap(View view, float x, float y) { if (mOnPhotoViewClickListener != null) { mOnPhotoViewClickListener.onPhotoViewClick(view, originViewBounds.get(currentPos), currentPos); } } }); } } }复制代码
在这里,我们留意到在回调里我们传入的rect就是外部传进来起始View的rect组,这里就回答了我们疑点中的第三个问题:
点击某张图片,滑动到其他图片时,退出的缩小动画如何缩小到对应的起始View中
我们的解决方法就是,把那个View的rect扔给我们的manager让他计算,就好了。
Step 5 - Item里使用
在目前的项目里,事实上也是在微信朋友圈里,图片永远都是0~9,在我们的项目中,因为ListView的Adapter高度抽象化,所以我们可以很轻松的在ViewHolder里处理
在ItemWithImg.java中,我们针对GridView的onItemClick进行处理:
public class ItemWithImg extends BaseItemDelegate implements AdapterView.OnItemClickListener { private static final String TAG = "ItemWithImg"; private NoScrollGridView mNoScrollGridView; private GridViewAdapter mGridViewAdapter; private ArrayListmUrls = new ArrayList<>(); private ArrayList mRects = new ArrayList<>(); ...略 @Override protected void bindData(int position, @NonNull View v, @NonNull MomentsInfo data, int dynamicType) { if (data.content.imgurl == null || data.content.imgurl.size() == 0 || mNoScrollGridView == null) return; mUrls.clear(); mUrls.addAll(data.content.imgurl); ...数据绑定 } @Override public void onItemClick(AdapterView parent, View view, int position, long id) { final int childCount = parent.getChildCount(); mRects.clear(); try { if (childCount >= 0) { for (int i = 0; i < childCount; i++) { View v = parent.getChildAt(i); Rect bound = new Rect(); v.getGlobalVisibleRect(bound); mRects.add(bound); } } } catch (NullPointerException e) { Log.e(TAG, "view可能为空哦"); } getPresenter().shoPhoto(mUrls, mRects, position); }}复制代码
这里我们需要留意两个地方:
- 在bindData里面,因为这个是一个抽象化的ViewHolder接口,所以事实上会在ListView的getView中不断的调用,而我们的url的arrayList是当前类的成员变量,所以我们每次都需要将其clear掉,否则数据只会累加,这样造成的就是图片数量与view的rect数组不对等。
- ItemClick里面,我们需要对parent拿到的view进行NPE捕获,否则挂掉就不好玩了。
到这里,我们的工作就完成了。
问题
花了那么多时间,终于把这个效果完成了,事实上最麻烦的东西都封到了manager里面,理论上来说要迁移到您的项目中也是非常简单的。
但目前来说,我们仅仅是初步实现了,其实有一些小问题还是存在的:
- ViewPager的adapter里面的view每次都是new,感觉有点浪费
- 由于上面的那个问题,导致假如我们在大于三张图或者分别点击不同的item时,放大动画会看不到,必须在载入一次图片后再次触发才会有。
- 不知道您有没有发现,其实我们缺少一个指示器,毕竟微信朋友圈在ViewPager下方可是有几个小点点的
虽然问题不是很大,但我们也有修复的理由对吧。
所以,在下一篇,我们将会针对这三个问题进行处理,以及关于PhotoView在ViewPager里面爆出的 "ImageView no longer exists. You should not use this PhotoViewAttacher any more." 错误从而导致PhotoView的点击事件无响应的处理方法。
敬请期待-V-