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

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


上篇鏈接:http://www.lxweimin.com/p/8984efce40ae
下篇鏈接:http://www.lxweimin.com/p/17c51bd5ba70


【Warning】:

本篇完整的從思考->尋找->編寫代碼->最終完成來闡述我如何實現本篇預覽圖功能
本篇篇幅較長,請帶上一定的耐心
本篇圖片較多,流量黨請注意
本篇比較抽象,我會盡量形象的闡述

本篇預覽圖:

preview.gif

前言

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

Zooming a View

點進去一看,瞬間滿滿的幸福感,原來頭疼了好久的問題,人家谷歌早就給出了答案

而且,不得不說的是,這個項目僅僅是在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 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的上方


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 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中,我們存放著這些參數:

  • 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 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的動畫起始值和目標值替換

  • 在放大動畫里,我們是從小->大,即計算出來的比率->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 ArrayList<String> photoAddress, @NonNull ArrayList<Rect> 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 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);
    }
}

這里我們需要留意兩個地方:

  • 在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-

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容