一起擼個(gè)朋友圈吧 - 圖片瀏覽(中)【圖片瀏覽器】

項(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ù)覽圖:

preview.gif

前言

正如上一篇文章所說压怠,一個(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)目:

Zooming a View

點(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)圖:

組織結(jié)構(gòu)

在點(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-

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末撬讽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子悬垃,更是在濱河造成了極大的恐慌游昼,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尝蠕,死亡現(xiàn)場(chǎng)離奇詭異烘豌,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)看彼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門廊佩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來囚聚,“玉大人,你說我怎么就攤上這事标锄⊥缰” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵料皇,是天一觀的道長谓松。 經(jīng)常有香客問我,道長践剂,這世上最難降的妖魔是什么鬼譬? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮舷手,結(jié)果婚禮上拧簸,老公的妹妹穿的比我還像新娘。我一直安慰自己男窟,他們只是感情好盆赤,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著歉眷,像睡著了一般牺六。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上汗捡,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天淑际,我揣著相機(jī)與錄音,去河邊找鬼扇住。 笑死春缕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的艘蹋。 我是一名探鬼主播锄贼,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼女阀!你這毒婦竟也來了宅荤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤浸策,失蹤者是張志新(化名)和其女友劉穎冯键,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庸汗,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惫确,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雕薪。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡昧诱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出所袁,到底是詐尸還是另有隱情,我是刑警寧澤凶掰,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布燥爷,位于F島的核電站,受9級(jí)特大地震影響懦窘,放射性物質(zhì)發(fā)生泄漏前翎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一畅涂、第九天 我趴在偏房一處隱蔽的房頂上張望港华。 院中可真熱鬧,春花似錦午衰、人聲如沸立宜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽橙数。三九已至,卻和暖如春帅戒,著一層夾襖步出監(jiān)牢的瞬間灯帮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工逻住, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钟哥,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓瞎访,卻偏偏與公主長得像腻贰,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子装诡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容