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

項(xiàng)目地址:https://github.com/razerdp/FriendCircle (能弱弱的求個(gè)star或者fork么QAQ)
《一起擼個(gè)朋友圈吧》 這是本文所處文集,所有更新都會(huì)在這個(gè)文集里面哦,歡迎關(guān)注


上篇鏈接:http://www.itdecent.cn/p/8984efce40ae
下篇鏈接:http://www.itdecent.cn/p/17c51bd5ba70


【W(wǎng)arning】:

本篇完整的從思考->尋找->編寫代碼->最終完成來(lái)闡述我如何實(shí)現(xiàn)本篇預(yù)覽圖功能
本篇篇幅較長(zhǎng),請(qǐng)帶上一定的耐心
本篇圖片較多,流量黨請(qǐng)注意
本篇比較抽象,我會(huì)盡量形象的闡述

本篇預(yù)覽圖:

preview.gif

前言

正如上一篇文章所說(shuō),一個(gè)app動(dòng)人之處在于細(xì)節(jié)的研磨和富有動(dòng)感的交互。

在微信的朋友圈,我們點(diǎn)擊圖片可以感覺到像預(yù)覽圖那樣的效果:點(diǎn)擊某張圖片,然后它會(huì)放大到全屏,再點(diǎn)擊,則會(huì)縮小到原來(lái)的那個(gè)地方

這種交互看起來(lái)非常贊,最起碼看得順眼。

然而很多時(shí)候交互動(dòng)作設(shè)計(jì)的時(shí)候看起來(lái)確實(shí)很棒,但對(duì)于我等程序員來(lái)說(shuō),設(shè)計(jì)棒,設(shè)計(jì)酷往往會(huì)讓我們擺出一張苦逼臉

—— 臣妾做不到啊,陛下。

但迫于Money的壓力下,我們往往不得不硬著頭皮上。

正如今天這個(gè)效果,確實(shí)一開始是沒(méi)有任何頭緒,在思考實(shí)現(xiàn)的過(guò)程中,我曾經(jīng)想過(guò)如下幾種方法:

  • 直接重寫一個(gè)ImageView,利用martix來(lái)放大圖片
  • 點(diǎn)擊的時(shí)候,通過(guò)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)畫。
  • 甚至想過(guò),直接上activityoptions....使用activity的轉(zhuǎn)場(chǎng)動(dòng)畫

但實(shí)際上,以上的方法貌似都可以,但實(shí)際上真要我去干了,就猶豫了,且不說(shuō)運(yùn)行效率,但起碼可以預(yù)測(cè)到代碼量。。。

然而,一次神奇的發(fā)現(xiàn),讓我解決了這個(gè)問(wèn)題,準(zhǔn)確的說(shuō),是谷歌早就解決了這個(gè)問(wèn)題。


羽翼君探索篇

發(fā)現(xiàn)

對(duì)于面向搜索引擎編程的我們,其實(shí)一直都習(xí)慣于有問(wèn)題找度娘,或者找谷歌。

鑒于度娘找到的技術(shù)文章基本都是你抄我,我抄你,于是我只好到谷歌以"android scale a view to full screen"來(lái)找答案,奈何找來(lái)找去都是關(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)去一看,瞬間滿滿的幸福感,原來(lái)頭疼了好久的問(wèn)題,人家谷歌早就給出了答案

而且,不得不說(shuō)的是,這個(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)這里看了幾次,于是決定,我必須要把這里所有東西弄明白。

這里真的要給谷歌一萬(wàn)個(gè)贊。


難點(diǎn)

在得到官方培訓(xùn)這個(gè)超級(jí)大外掛后,最難的地方其實(shí)已經(jīng)沒(méi)有什么障礙了,剩下的就是該如何適配到我們的項(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中,僅僅只有一張圖片的瀏覽,也就是說(shuō)僅僅是展示了一張圖片縮放到全屏的方法,所以我們只有去完全的理解demo,才能繼續(xù)我們的工程。

不過(guò)在真正實(shí)現(xiàn)之前,上面的問(wèn)題我們其實(shí)可以回答幾個(gè):

  • 針對(duì)第一個(gè)問(wèn)題,我們可以在點(diǎn)擊圖片的時(shí)候,把當(dāng)前圖片所在的Item的圖片地址數(shù)組傳到adapter里面然后通知更新

  • 在我們使用朋友圈的時(shí)候,可以感覺點(diǎn)擊圖片放大這個(gè)過(guò)程非常的快,而如果重新打開一個(gè)Activity,則需要經(jīng)過(guò)那么多的onCreate等生命期方法,那么肯定不會(huì)有這么靈敏的反應(yīng),所以很明顯,這個(gè)ViewPager其實(shí)是包含在朋友圈所在的窗口,只是平時(shí)隱藏起來(lái)而已。

  • 針對(duì)第三個(gè)問(wèn)題在稍后的闡述中回答。

  • 第四個(gè)問(wèn)題,我覺得想都不用想,直接上PhotoView這個(gè)庫(kù)。


原理篇

官方Demo詳解

事實(shí)上,官方的demo中的注釋是十分的清楚的,官方的Demo最主要依靠的是兩個(gè)東西:

  • getGlobalVisibleRect
  • ObjectAnimator

我們都知道,一個(gè)View是可以通過(guò)getLocationOnScreen或者**getLocationInWindow **得到相對(duì)于整個(gè)屏幕/相對(duì)于父控件的xy位置信息。

getGlobalVisibleRect/getLocalVisibleRect跟上面的這個(gè)其實(shí)差不多,不過(guò)不同的是,它得到的不是xy位置信息,而是得到指定View在屏幕中展示的矩形信息

簡(jiǎn)單的描述就是前者得到view的原點(diǎn)信息,后者得到view的2D形狀信息。

官方的demo則是通過(guò)這個(gè)得到兩個(gè)view的Rect:

  • 點(diǎn)擊的view的rect(startRect
  • 最終放大后的view的rect(endRect

得到兩個(gè)view的矩形后,就可以得到雙方的縮放比。

通過(guò)這個(gè)比例,可以做的事情就很多了,官方的demo則是通過(guò)這個(gè)比例,來(lái)計(jì)算出以下的參數(shù):

  • 小圖的x位置和大圖的x位置,得到放大后View水平方向上應(yīng)該偏移多少。
    • 因?yàn)閮H僅是view的縮放是不夠的,因?yàn)樾枰WC放大后的View處于屏幕中央,而不是說(shuō)偏左偏右。
  • 同理得到垂直方向的偏移。
  • 得到水平方向的縮放比,也就是上面所說(shuō)的縮放比。
  • 同理得到垂直方向的縮放比。

得到這些參數(shù)后,就通過(guò)ObjectAnimator,操作的對(duì)象是隱藏在屏幕中的最終展示的View,通過(guò)監(jiān)聽它的數(shù)值變化,從而不斷的更新展示的View的屬性,給人造成原來(lái)的view放大的錯(cuò)覺。

或許文字說(shuō)的有點(diǎn)枯燥,所以,直接上AE,弄出一個(gè)動(dòng)圖,相信大家一看就明白:

在圖層的結(jié)構(gòu)上如下動(dòng)圖:

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

在點(diǎn)擊的之后,會(huì)發(fā)生如下動(dòng)作:

過(guò)程

總結(jié)起來(lái)就是:

  • 點(diǎn)擊時(shí),得到被點(diǎn)擊的view的rect,和最終效果的rect
  • 通過(guò)兩個(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è)方法,這里就不貼上來(lái)了。

接下啦到我們朋友圈的布局中,添加一個(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中,我們只管視圖的渲染,不管事件的處理,事件的處理我們通過(guò)接口拋到外部處理。

因此我們的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í)先不管,接下來(lái)我們需要處理的就是縮放動(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來(lái)進(jìn)行刷新以及setCurrentItem來(lái)定位到我們點(diǎn)擊的圖片位于圖片序列的位置
  • container:就是布局里包裹著ViewPager的RelativeLayout,我們通過(guò)它做一個(gè)點(diǎn)擊消逝時(shí)的透明度漸變動(dòng)畫,同時(shí)endRect也是依靠它來(lái)得到

由此,我們初步設(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)畫沒(méi)展示完,就將其取消以執(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的接口。

接下來(lái)我們補(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來(lái)進(jìn)行刷新。

接下來(lái)判斷傳進(jìn)來(lái)的圖片是否只有一張,如果只有一張,就不允許viewpager滑動(dòng),setLocked方法是PhotoView作者給出的解決方案帶有的,其原理是在Viewpager的onInterceptTouchEvent里通過(guò)locked來(lái)決定是否攔截事件。

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ì)闡述了。

不過(guò)值得留意的是,在動(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)畫沒(méi)有展示完全就關(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ì)算出來(lái)的比率->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的寬高比
  • 無(wú)論是那種,都需要計(jì)算出差值,這個(gè)差值用來(lái)定位最終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)來(lái)起始View的rect組,這里就回答了我們疑點(diǎn)中的第三個(gè)問(wèn)題:

點(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)長(zhǎng)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捕獲,否則掛掉就不好玩了。

到這里,我們的工作就完成了。


問(wèn)題

花了那么多時(shí)間,終于把這個(gè)效果完成了,事實(shí)上最麻煩的東西都封到了manager里面,理論上來(lái)說(shuō)要遷移到您的項(xiàng)目中也是非常簡(jiǎn)單的。

但目前來(lái)說(shuō),我們僅僅是初步實(shí)現(xiàn)了,其實(shí)有一些小問(wèn)題還是存在的:

  • ViewPager的adapter里面的view每次都是new,感覺有點(diǎn)浪費(fèi)
  • 由于上面的那個(gè)問(wèn)題,導(dǎo)致假如我們?cè)诖笥谌龔垐D或者分別點(diǎn)擊不同的item時(shí),放大動(dòng)畫會(huì)看不到,必須在載入一次圖片后再次觸發(fā)才會(huì)有。
  • 不知道您有沒(méi)有發(fā)現(xiàn),其實(shí)我們?nèi)鄙僖粋€(gè)指示器,畢竟微信朋友圈在ViewPager下方可是有幾個(gè)小點(diǎn)點(diǎn)的

雖然問(wèn)題不是很大,但我們也有修復(fù)的理由對(duì)吧。

所以,在下一篇,我們將會(huì)針對(duì)這三個(gè)問(wèn)題進(jìn)行處理,以及關(guān)于PhotoView在ViewPager里面爆出的"ImageView no longer exists. You should not use this PhotoViewAttacher any more."錯(cuò)誤從而導(dǎo)致PhotoView的點(diǎn)擊事件無(wú)響應(yīng)的處理方法。

敬請(qǐng)期待-V-

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容