項(xiàng)目地址:https://github.com/razerdp/FriendCircle (能弱弱的求個(gè)star或者fork么QAQ)
《一起擼個(gè)朋友圈吧》 這是本文所處文集,所有更新都會(huì)在這個(gè)文集里面哦,歡迎關(guān)注
上篇鏈接:http://www.reibang.com/p/8984efce40ae
下篇鏈接:http://www.reibang.com/p/17c51bd5ba70
【W(wǎng)arning】:
本篇完整的從思考->尋找->編寫代碼->最終完成
來闡述我如何實(shí)現(xiàn)本篇預(yù)覽圖功能
本篇篇幅較長绩社,請(qǐng)帶上一定的耐心
本篇圖片較多幢尚,流量黨請(qǐng)注意
本篇比較抽象,我會(huì)盡量形象的闡述
本篇預(yù)覽圖:
前言
正如上一篇文章所說压怠,一個(gè)app動(dòng)人之處在于細(xì)節(jié)的研磨和富有動(dòng)感的交互。
在微信的朋友圈,我們點(diǎn)擊圖片可以感覺到像預(yù)覽圖那樣的效果:點(diǎn)擊某張圖片崇败,然后它會(huì)放大到全屏,再點(diǎn)擊肩祥,則會(huì)縮小到原來的那個(gè)地方
這種交互看起來非常贊后室,最起碼看得順眼微渠。
然而很多時(shí)候交互動(dòng)作設(shè)計(jì)的時(shí)候看起來確實(shí)很棒轴脐,但對(duì)于我等程序員來說鲁森,設(shè)計(jì)棒,設(shè)計(jì)酷往往會(huì)讓我們擺出一張苦逼臉
—— 臣妾做不到啊绪钥,陛下松申。
但迫于Money的壓力下云芦,我們往往不得不硬著頭皮上。
正如今天這個(gè)效果贸桶,確實(shí)一開始是沒有任何頭緒舅逸,在思考實(shí)現(xiàn)的過程中,我曾經(jīng)想過如下幾種方法:
- 直接重寫一個(gè)ImageView皇筛,利用martix來放大圖片
- 點(diǎn)擊的時(shí)候琉历,通過windowmanager動(dòng)態(tài)添加一個(gè)imageview,然后讓這個(gè)imageview實(shí)現(xiàn)動(dòng)畫
- 弄兩個(gè)view水醋,一個(gè)隱藏的旗笔,一個(gè)是listview中的,然后點(diǎn)擊的時(shí)候把隱藏的顯示拄踪,并進(jìn)行動(dòng)畫蝇恶。
- 甚至想過,直接上activityoptions....使用activity的轉(zhuǎn)場(chǎng)動(dòng)畫
但實(shí)際上惶桐,以上的方法貌似都可以撮弧,但實(shí)際上真要我去干了,就猶豫了姚糊,且不說運(yùn)行效率贿衍,但起碼可以預(yù)測(cè)到代碼量。救恨。贸辈。
然而,一次神奇的發(fā)現(xiàn)忿薇,讓我解決了這個(gè)問題裙椭,準(zhǔn)確的說,是谷歌早就解決了這個(gè)問題署浩。
羽翼君探索篇
發(fā)現(xiàn)
對(duì)于面向搜索引擎編程的我們揉燃,其實(shí)一直都習(xí)慣于有問題找度娘,或者找谷歌筋栋。
鑒于度娘找到的技術(shù)文章基本都是你抄我炊汤,我抄你,于是我只好到谷歌以"android scale a view to full screen"來找答案,奈何找來找去都是關(guān)于如何讓imageview的圖片填充整個(gè)屏幕的抢腐。
于是換個(gè)思路姑曙,除了scale,我們不是經(jīng)常還能接觸到"zoom"這個(gè)關(guān)鍵詞么迈倍,于是就繼續(xù)谷歌"android zoom a view to full screen"
結(jié)果第一個(gè)結(jié)果就是Android開發(fā)者文檔的train項(xiàng)目:
點(diǎn)進(jìn)去一看伤靠,瞬間滿滿的幸福感,原來頭疼了好久的問題啼染,人家谷歌早就給出了答案
而且宴合,不得不說的是,這個(gè)項(xiàng)目?jī)H僅是在Android的培訓(xùn)項(xiàng)目迹鹅,相當(dāng)于打游戲第一關(guān)的新手教程那樣吧卦洽,具體地址可以點(diǎn)這里(http://developer.android.com/intl/zh-cn/training/index.html )
事實(shí)上,在完成了這篇文章的效果后斜棚,我到官方培訓(xùn)這里看了幾次阀蒂,于是決定,我必須要把這里所有東西弄明白弟蚀。
這里真的要給谷歌一萬個(gè)贊蚤霞。
難點(diǎn)
在得到官方培訓(xùn)這個(gè)超級(jí)大外掛后,最難的地方其實(shí)已經(jīng)沒有什么障礙了粗梭,剩下的就是該如何適配到我們的項(xiàng)目中争便。
從我們?nèi)粘J褂门笥讶Φ慕?jīng)驗(yàn)看,關(guān)于圖片點(diǎn)擊放大會(huì)涉及到這么幾個(gè)難點(diǎn):
朋友圈的圖片是1~9張断医,那么該如何確保ViewPager可以加載相等數(shù)量的圖片
點(diǎn)擊圖片是否應(yīng)該跳轉(zhuǎn)到新的Activity
假如我點(diǎn)擊第一張圖片,在ViewPager滑倒第三張圖片奏纪,那么點(diǎn)擊圖片退出時(shí)該如何確保View縮小后的位置與第三張圖片一致而非縮小到第一張圖片的位置鉴嗤。
ViewPager瀏覽的時(shí)候圖片放大和縮小如何實(shí)現(xiàn)
在官方的demo中,僅僅只有一張圖片的瀏覽序调,也就是說僅僅是展示了一張圖片縮放到全屏的方法醉锅,所以我們只有去完全的理解demo,才能繼續(xù)我們的工程发绢。
不過在真正實(shí)現(xiàn)之前硬耍,上面的問題我們其實(shí)可以回答幾個(gè):
針對(duì)第一個(gè)問題,我們可以在點(diǎn)擊圖片的時(shí)候边酒,把當(dāng)前圖片所在的Item的圖片地址數(shù)組傳到adapter里面然后通知更新
在我們使用朋友圈的時(shí)候经柴,可以感覺點(diǎn)擊圖片放大這個(gè)過程非常的快,而如果重新打開一個(gè)Activity墩朦,則需要經(jīng)過那么多的onCreate等生命期方法坯认,那么肯定不會(huì)有這么靈敏的反應(yīng),所以很明顯,這個(gè)ViewPager其實(shí)是包含在朋友圈所在的窗口牛哺,只是平時(shí)隱藏起來而已陋气。
針對(duì)第三個(gè)問題在稍后的闡述中回答。
第四個(gè)問題引润,我覺得想都不用想巩趁,直接上PhotoView這個(gè)庫。
原理篇
官方Demo詳解
事實(shí)上淳附,官方的demo中的注釋是十分的清楚的议慰,官方的Demo最主要依靠的是兩個(gè)東西:
- getGlobalVisibleRect
- ObjectAnimator
我們都知道,一個(gè)View是可以通過getLocationOnScreen或者**getLocationInWindow **得到相對(duì)于整個(gè)屏幕/相對(duì)于父控件的xy位置信息燃观。
而getGlobalVisibleRect/getLocalVisibleRect跟上面的這個(gè)其實(shí)差不多褒脯,不過不同的是,它得到的不是xy位置信息缆毁,而是得到指定View在屏幕中展示的矩形信息
簡(jiǎn)單的描述就是前者得到view的原點(diǎn)信息番川,后者得到view的2D形狀信息。
官方的demo則是通過這個(gè)得到兩個(gè)view的Rect:
- 點(diǎn)擊的view的rect(startRect)
- 最終放大后的view的rect(endRect)
得到兩個(gè)view的矩形后脊框,就可以得到雙方的縮放比颁督。
通過這個(gè)比例,可以做的事情就很多了浇雹,官方的demo則是通過這個(gè)比例沉御,來計(jì)算出以下的參數(shù):
- 小圖的x位置和大圖的x位置,得到放大后View水平方向上應(yīng)該偏移多少昭灵。
- 因?yàn)閮H僅是view的縮放是不夠的吠裆,因?yàn)樾枰WC放大后的View處于屏幕中央,而不是說偏左偏右烂完。
- 同理得到垂直方向的偏移试疙。
- 得到水平方向的縮放比,也就是上面所說的縮放比抠蚣。
- 同理得到垂直方向的縮放比祝旷。
得到這些參數(shù)后,就通過ObjectAnimator嘶窄,操作的對(duì)象是隱藏在屏幕中的最終展示的View怀跛,通過監(jiān)聽它的數(shù)值變化,從而不斷的更新展示的View的屬性柄冲,給人造成原來的view放大的錯(cuò)覺吻谋。
或許文字說的有點(diǎn)枯燥,所以羊初,直接上AE滨溉,弄出一個(gè)動(dòng)圖什湘,相信大家一看就明白:
在圖層的結(jié)構(gòu)上如下動(dòng)圖:
在點(diǎn)擊的之后,會(huì)發(fā)生如下動(dòng)作:
總結(jié)起來就是:
- 點(diǎn)擊時(shí)晦攒,得到被點(diǎn)擊的view的rect闽撤,和最終效果的rect
- 通過兩個(gè)rect計(jì)算縮放比率
- 動(dòng)畫開始之前將最終展示的view設(shè)置為可見
- 由于ObjectAnimator開始的值是被點(diǎn)擊的view的rect值,所以最終展示的view會(huì)從被點(diǎn)擊的view的大小開始播放
- 隨著動(dòng)畫進(jìn)行脯颜,不斷的改變自己的x,y,scaleX,scaleY
- 從而達(dá)到讓人感覺view從小變大的錯(cuò)覺
代碼篇
Step 1- 布局/MVP的方法添加
呼呼哟旗,又是AE,又是穹妹(手動(dòng)斜眼)的栋操,終于可以開始弄我們的代碼了闸餐。
從上面我們知道,要實(shí)現(xiàn)這種偽放大效果矾芙,最重要的是得到開始和結(jié)束兩個(gè)view的rect舍沙,而我們由于是使用viewpager,所以我們穿值傳遞的是一個(gè)數(shù)組剔宪,這個(gè)數(shù)組就是當(dāng)前Item所擁有的imageview的rect數(shù)組拂铡。
因此回到我們的Activity,由于我們采用MVP模式葱绒,所以在View層增加一個(gè)方法:
public interface DynamicView {
...之前的方法不變
// 瀏覽圖片
void showPhoto(@NonNull ArrayList<String> photoAddress, @NonNull ArrayList<Rect> originViewBounds, int
curSelectedPos);
}
同樣在P層也增加這個(gè)方法感帅,這里就不貼上來了。
接下啦到我們朋友圈的布局中地淀,添加一個(gè)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>
值得注意的是,因?yàn)槲⑿排笥讶Φ拇髨D瀏覽是有背景(黑色)的帮毁,所以我們外層用一個(gè)布局包裹实苞。
另外由于我們需要使用PhotoView,所以我們的ViewPager將會(huì)使用PhotoView作者給出的解決方法:
HackyViewPager代碼如下(因?yàn)橛蠰ICENSE烈疚,所以就完整貼出):
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的上方
Step 2 - ViewPager的adapter
adapter很明顯胞得,就是為了實(shí)現(xiàn)我們的所有方法的,在adapter的設(shè)計(jì)中屹电,我們需要知道幾個(gè)地方:
-
ViewPager的adapter如果直接調(diào)用adapter.notifydatasetchanged是未必能刷新的阶剑,這個(gè)跟getItemPosition方法有關(guān),所以如果想adapter刷新危号,就需要覆寫這個(gè)牧愁。
- 本例的adapter刷新不使用notifydatasetchanged,而是直接setAdapter外莲,viewpager的setAdapter方法會(huì)觸發(fā)destroyItem猪半,所以我們直接使用setAdapter
adapter中兔朦,我們只管視圖的渲染,不管事件的處理磨确,事件的處理我們通過接口拋到外部處理沽甥。
因此我們的adapter將會(huì)這么設(shè)計(jì):
/**
* 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("圖片地址和圖片的位置緩存不對(duì)等或某一個(gè)為空");
}
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;
}
//=============================================================點(diǎn)擊消失的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中,我們存放著這些參數(shù):
- photoAddress:存放圖片地址的數(shù)組
- originViewBounds:點(diǎn)擊時(shí)所處item的所有imageview的rect
然后還有我們內(nèi)部定義的接口:OnPhotoViewClickListener乏奥,這個(gè)接口在點(diǎn)擊Viewpager里面的PhotoView時(shí)會(huì)觸發(fā)摆舟。
Step 3 - PhotoPagerManager
在adapter初步結(jié)構(gòu)設(shè)計(jì)后,我們暫時(shí)先不管邓了,接下來我們需要處理的就是縮放動(dòng)畫和點(diǎn)擊的事件處理恨诱。
由于我們的Activity作為MVP的View,代碼量已經(jīng)比較多了骗炉,所以我們將動(dòng)畫的實(shí)現(xiàn)和點(diǎn)擊事件的處理封裝到另一個(gè)類里照宝,委托它進(jìn)行操作。
在設(shè)計(jì)這個(gè)類之前句葵,我們需要確定一下需要的委托管理的東西:
- adapter:負(fù)責(zé)處理adapter內(nèi)部PhotoView點(diǎn)擊時(shí)回調(diào)的動(dòng)作
- pager:需要pager的setAdapter來進(jìn)行刷新以及setCurrentItem來定位到我們點(diǎn)擊的圖片位于圖片序列的位置
- container:就是布局里包裹著ViewPager的RelativeLayout厕鹃,我們通過它做一個(gè)點(diǎn)擊消逝時(shí)的透明度漸變動(dòng)畫,同時(shí)endRect也是依靠它來得到
由此笼呆,我們初步設(shè)計(jì)以下結(jié)構(gòu):
/**
* Created by 大燈泡 on 2016/4/12.
* 相冊(cè)展示的管理類
*/
public class PhotoPagerManager implements PhotoBoswerPagerAdapter.OnPhotoViewClickListener {
private Context mContext;
private PhotoBoswerPagerAdapter adapter;
private HackyViewPager pager;
private Rect finalBounds;
private Point globalOffset;
private View container;
//私有構(gòu)造器
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不能為空哦");
}
}
//靜態(tài)工廠
public static PhotoPagerManager create(Context context, HackyViewPager pager, View container) {
return new PhotoPagerManager(context, pager, container);
}
//共有調(diào)用方法熊响,傳入圖片地址和view的可見矩形數(shù)組
public void showPhoto(
@NonNull ArrayList<String> photoAddress, @NonNull ArrayList<Rect> originViewBounds, int curSelectedPos) {
}
//當(dāng)前正在進(jìn)行的動(dòng)畫,如果動(dòng)畫沒展示完诗赌,就將其取消以執(zhí)行下一個(gè)動(dòng)畫
private AnimatorSet curAnimator;
//私有showPhoto處理
private void showPhotoPager(@NonNull ArrayList<Rect> originViewBounds, int curSelectedPos) {
}
//pager的PhotoView點(diǎn)擊回調(diào)汗茄,用于執(zhí)行消失時(shí)的縮小動(dòng)畫
@Override
public void onPhotoViewClick(View view, Rect originBound, int curPos) {
}
//計(jì)算縮放比率
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里面
在私有構(gòu)造器里面我們將需要的成員進(jìn)行賦值铭若,同時(shí)adapter需要實(shí)現(xiàn)我們?cè)诘诙蕉x的接口洪碳。
接下來我們補(bǔ)充共有的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);
}
每次調(diào)用show方法我們都需要刷新adapter的數(shù)據(jù),然后使用setAdapter來進(jìn)行刷新叼屠。
接下來判斷傳進(jìn)來的圖片是否只有一張瞳腌,如果只有一張,就不允許viewpager滑動(dòng)镜雨,setLocked方法是PhotoView作者給出的解決方案帶有的嫂侍,其原理是在Viewpager的onInterceptTouchEvent里通過locked來決定是否攔截事件。
container.getGlobalVisibleRect(finalBounds, globalOffset);
這個(gè)在上面的解釋里已經(jīng)有荚坞,這里只是直接copy官方demo代碼而已挑宠。
最后調(diào)用私有方法: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();
}
這里跟官方的代碼基本一致,因?yàn)楣俜酱a有注釋颓影,所以這里就不詳細(xì)闡述了各淀。
不過值得留意的是,在動(dòng)畫執(zhí)行之前必須要將container的alpha設(shè)回1诡挂,因?yàn)槲覀冊(cè)谕顺鰟?dòng)畫里將它設(shè)置為0的碎浇。
同理临谱,在PhotoView點(diǎn)擊回調(diào)里,我們也寫出差不多的代碼:
@Override
public void onPhotoViewClick(View view, Rect originBound, int curPos) {
//如果展開動(dòng)畫沒有展示完全就關(guān)閉奴璃,那么就停止展開動(dòng)畫進(jìn)而執(zhí)行退出動(dòng)畫
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();
}
在退出的動(dòng)畫里悉默,我們需要將SCALE_X和SCALE_Y的動(dòng)畫起始值和目標(biāo)值替換
- 在放大動(dòng)畫里,我們是從小->大溺健,即計(jì)算出來的比率->1f
- 在縮小動(dòng)畫則相反麦牺,從大到小
最后補(bǔ)全,哦鞭缭,不剖膳,是copy官方的計(jì)算比率的方法:
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;
}
官方的計(jì)算方法是這樣的:
- 比較最終view的寬高比和起始view的寬高比
- 無論是那種,都需要計(jì)算出差值岭辣,這個(gè)差值用來定位最終view動(dòng)畫播放時(shí)的起始位置吱晒,讓其保證跟起始view一致
在這個(gè)類完成后,我們?cè)贏ctivity里僅僅需要兩句話調(diào)用:
/**
* 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);
}
}
Step 4 - adapter代碼補(bǔ)全
實(shí)現(xiàn)完manager后沦童,我們就補(bǔ)全我們的adapter代碼
在adapter里面仑濒,我們主要關(guān)注兩個(gè)方法:
- instantiateItem:初始化view的時(shí)候回調(diào)
- setPrimaryItem:滑動(dòng)時(shí)回調(diào)當(dāng)前展示著的view
其他方法都是常規(guī)方法,就不展示了
初始化的時(shí)候偷遗,我們的代碼非常簡(jiǎn)單墩瞳,new一個(gè),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中泪电,我們?yōu)閜hotoView設(shè)置回調(diào):
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);
}
}
});
}
}
}
在這里,我們留意到在回調(diào)里我們傳入的rect就是外部傳進(jìn)來起始View的rect組纪铺,這里就回答了我們疑點(diǎn)中的第三個(gè)問題:
點(diǎn)擊某張圖片相速,滑動(dòng)到其他圖片時(shí),退出的縮小動(dòng)畫如何縮小到對(duì)應(yīng)的起始View中
我們的解決方法就是鲜锚,把那個(gè)View的rect扔給我們的manager讓他計(jì)算突诬,就好了。
Step 5 - Item里使用
在目前的項(xiàng)目里芜繁,事實(shí)上也是在微信朋友圈里攒霹,圖片永遠(yuǎn)都是0~9,在我們的項(xiàng)目中浆洗,因?yàn)長istView的Adapter高度抽象化,所以我們可以很輕松的在ViewHolder里處理
在ItemWithImg.java中集峦,我們針對(duì)GridView的onItemClick進(jìn)行處理:
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);
...數(shù)據(jù)綁定
}
@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);
}
}
這里我們需要留意兩個(gè)地方:
- 在bindData里面伏社,因?yàn)檫@個(gè)是一個(gè)抽象化的ViewHolder接口抠刺,所以事實(shí)上會(huì)在ListView的getView中不斷的調(diào)用,而我們的url的arrayList是當(dāng)前類的成員變量摘昌,所以我們每次都需要將其clear掉速妖,否則數(shù)據(jù)只會(huì)累加,這樣造成的就是圖片數(shù)量與view的rect數(shù)組不對(duì)等聪黎。
- ItemClick里面罕容,我們需要對(duì)parent拿到的view進(jìn)行NPE捕獲,否則掛掉就不好玩了稿饰。
到這里锦秒,我們的工作就完成了。
問題
花了那么多時(shí)間喉镰,終于把這個(gè)效果完成了旅择,事實(shí)上最麻煩的東西都封到了manager里面,理論上來說要遷移到您的項(xiàng)目中也是非常簡(jiǎn)單的侣姆。
但目前來說生真,我們僅僅是初步實(shí)現(xiàn)了,其實(shí)有一些小問題還是存在的:
- ViewPager的adapter里面的view每次都是new捺宗,感覺有點(diǎn)浪費(fèi)
- 由于上面的那個(gè)問題柱蟀,導(dǎo)致假如我們?cè)诖笥谌龔垐D或者分別點(diǎn)擊不同的item時(shí),放大動(dòng)畫會(huì)看不到蚜厉,必須在載入一次圖片后再次觸發(fā)才會(huì)有长已。
- 不知道您有沒有發(fā)現(xiàn),其實(shí)我們?nèi)鄙僖粋€(gè)指示器弯囊,畢竟微信朋友圈在ViewPager下方可是有幾個(gè)小點(diǎn)點(diǎn)的
雖然問題不是很大痰哨,但我們也有修復(fù)的理由對(duì)吧。
所以匾嘱,在下一篇斤斧,我們將會(huì)針對(duì)這三個(gè)問題進(jìn)行處理,以及關(guān)于PhotoView在ViewPager里面爆出的"ImageView no longer exists. You should not use this PhotoViewAttacher any more."錯(cuò)誤從而導(dǎo)致PhotoView的點(diǎn)擊事件無響應(yīng)的處理方法霎烙。
敬請(qǐng)期待-V-