项目地址:github.com/razerdp/Fri… (能弱弱的求个star或者fork么QAQ)php
上篇连接:一块儿撸个朋友圈吧 - 图片浏览(上)【图片点击前景色】html
下篇连接:一块儿撸个朋友圈吧 - 图片浏览(下)【ViewPager优化】java
【Warning】:android
本篇完整的从思考->寻找->编写代码->最终完成
来阐述我如何实现本篇预览图功能git
本篇篇幅较长,请带上必定的耐心程序员
本篇图片较多,流量党请注意github
本篇比较抽象,我会尽可能形象的阐述编程
本篇预览图:数组
正如上一篇文章所说,一个app动人之处在于细节的研磨和富有动感的交互。缓存
在微信的朋友圈,咱们点击图片能够感受到像预览图那样的效果:点击某张图片,而后它会放大到全屏,再点击,则会缩小到原来的那个地方
这种交互看起来很是赞,最起码看得顺眼。
然而不少时候交互动做设计的时候看起来确实很棒,但对于我等程序员来讲,设计棒,设计酷每每会让咱们摆出一张苦逼脸
—— 臣妾作不到啊,陛下。
但迫于Money的压力下,咱们每每不得不硬着头皮上。
正现在天这个效果,确实一开始是没有任何头绪,在思考实现的过程当中,我曾经想过以下几种方法:
但实际上,以上的方法貌似均可以,但实际上真要我去干了,就犹豫了,且不说运行效率,但起码能够预测到代码量。。。
然而,一次神奇的发现,让我解决了这个问题,准确的说,是谷歌早就解决了这个问题。
对于面向搜索引擎编程的咱们,其实一直都习惯于有问题找度娘,或者找谷歌。
鉴于度娘找到的技术文章基本都是你抄我,我抄你,因而我只好到谷歌以**"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最主要依靠的是两个东西:
咱们都知道,一个View是能够经过getLocationOnScreen或者**getLocationInWindow **获得相对于整个屏幕/相对于父控件的xy位置信息。
而getGlobalVisibleRect/getLocalVisibleRect跟上面的这个其实差很少,不过不一样的是,它获得的不是xy位置信息,而是获得指定View在屏幕中展现的矩形信息
简单的描述就是前者获得view的原点信息,后者获得view的2D形状信息。
官方的demo则是经过这个获得两个view的Rect:
获得两个view的矩形后,就能够获得双方的缩放比。
经过这个比例,能够作的事情就不少了,官方的demo则是经过这个比例,来计算出如下的参数:
获得这些参数后,就经过ObjectAnimator,操做的对象是隐藏在屏幕中的最终展现的View,经过监听它的数值变化,从而不断的更新展现的View的属性,给人形成原来的view放大的错觉。
或许文字说的有点枯燥,因此,直接上AE,弄出一个动图,相信你们一看就明白:
在图层的结构上以下动图:
在点击的以后,会发生以下动做:
总结起来就是:
呼呼,又是AE,又是穹妹(手动斜眼)的,终于能够开始弄咱们的代码了。
从上面咱们知道,要实现这种伪放大效果,最重要的是获得开始和结束两个view的rect,而咱们因为是使用viewpager,因此咱们穿值传递的是一个数组,这个数组就是当前Item所拥有的imageview的rect数组。
所以回到咱们的Activity,因为咱们采用MVP模式,因此在View层增长一个方法:
public interface DynamicView {
...以前的方法不变
// 浏览图片
void showPhoto(@NonNull ArrayList<String> photoAddress, @NonNull ArrayList<Rect> originViewBounds, int curSelectedPos);
}
复制代码
一样在P层也增长这个方法,这里就不贴上来了。
接下啦到咱们朋友圈的布局中,添加一个viewpager。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/photo_container" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/black" android:visibility="invisible">
<razerdp.friendcircle.widget.HackyViewPager android:id="@+id/photo_pager" android:layout_width="match_parent" android:layout_height="match_parent"/>
</RelativeLayout>
复制代码
值得注意的是,由于微信朋友圈的大图浏览是有背景(黑色)的,因此咱们外层用一个布局包裹。
另外因为咱们须要使用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 * <p/> * 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. * <p/> * 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的上方
adapter很明显,就是为了实现咱们的全部方法的,在adapter的设计中,咱们须要知道几个地方:
ViewPager的adapter若是直接调用adapter.notifydatasetchanged是未必能刷新的,这个跟getItemPosition方法有关,因此若是想adapter刷新,就须要覆写这个。
adapter中,咱们只管视图的渲染,无论事件的处理,事件的处理咱们经过接口抛到外部处理。
所以咱们的adapter将会这么设计:
/** * Created by 大灯泡 on 2016/4/12. * 图片浏览窗口的adapter */
public class PhotoBoswerPagerAdapter extends PagerAdapter {
private static final String TAG = "PhotoBoswerPagerAdapter";
//=============================================================datas
private ArrayList<String> photoAddress;
private ArrayList<Rect> 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<String> newAddress, @NonNull ArrayList<Rect> 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中,咱们存放着这些参数:
而后还有咱们内部定义的接口:OnPhotoViewClickListener,这个接口在点击Viewpager里面的PhotoView时会触发。
在adapter初步结构设计后,咱们暂时先无论,接下来咱们须要处理的就是缩放动画和点击的事件处理。
因为咱们的Activity做为MVP的View,代码量已经比较多了,因此咱们将动画的实现和点击事件的处理封装到另外一个类里,委托它进行操做。
在设计这个类以前,咱们须要肯定一下须要的委托管理的东西:
由此,咱们初步设计如下结构:
/** * 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 ArrayList<String> photoAddress, @NonNull ArrayList<Rect> originViewBounds, int curSelectedPos) {
}
//当前正在进行的动画,若是动画没展现完,就将其取消以执行下一个动画
private AnimatorSet curAnimator;
//私有showPhoto处理
private void showPhotoPager(@NonNull ArrayList<Rect> 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 ArrayList<String> photoAddress, @NonNull ArrayList<Rect> 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 ArrayList<Rect> originViewBounds, 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的动画起始值和目标值替换
最后补全,哦,不,是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;
}
复制代码
官方的计算方法是这样的:
在这个类完成后,咱们在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 ArrayList<String> photoAddress, @NonNull ArrayList<Rect> originViewBounds, int curSelectedPos) {
//事件委托给manager
mPhotoPagerManager.showPhoto(photoAddress, originViewBounds, curSelectedPos);
}
}
复制代码
实现完manager后,咱们就补全咱们的adapter代码
在adapter里面,咱们主要关注两个方法:
其余方法都是常规方法,就不展现了
初始化的时候,咱们的代码很是简单,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让他计算,就行了。
在目前的项目里,事实上也是在微信朋友圈里,图片永远都是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 ArrayList<String> mUrls = new ArrayList<>();
private ArrayList<Rect> 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);
}
}
复制代码
这里咱们须要留意两个地方:
到这里,咱们的工做就完成了。
花了那么多时间,终于把这个效果完成了,事实上最麻烦的东西都封到了manager里面,理论上来讲要迁移到您的项目中也是很是简单的。
但目前来讲,咱们仅仅是初步实现了,其实有一些小问题仍是存在的:
虽然问题不是很大,但咱们也有修复的理由对吧。
因此,在下一篇,咱们将会针对这三个问题进行处理,以及关于PhotoView在ViewPager里面爆出的 "ImageView no longer exists. You should not use this PhotoViewAttacher any more." 错误从而致使PhotoView的点击事件无响应的处理方法。
敬请期待-V-