一塊兒擼個朋友圈吧 - 圖片瀏覽(下)【ViewPager優化】

項目地址:github.com/razerdp/Fri… (能弱弱的求個star或者fork麼QAQ)java


【ps:評論功能羽翼君我補全了後臺交互了喲,若是您想體驗一下不一樣的用戶而不是一直都是羽翼君,能夠在FriendCircleApp下,在onCreate中,將LocalHostInfo.INSTANCE.setHostId(1001);的id改成1001~1115之間任意一個】github

在上一篇,咱們實現了朋友圈的圖片瀏覽,在文章的最後,留下了幾個問題,那麼這一片咱們解決這些。canvas

本篇須要解決的幾個問題(本篇主要爲控件的自定義,但相信我,不會很難):數組

- viewpager如何複用瀏覽器

- 圖片瀏覽viewpager的指示器緩存

本篇圖片預覽以下:微信

preview

Q1:指示器

咱們知道,在微信圖片瀏覽的時候,多張圖下方是有個指示器的,好比這樣app

固然,咱們能夠找庫,但這個如此簡單的控件爲此花時間去找庫,倒不如咱們本身來定製一番對吧。

咱們來分析一下,能夠如何實現這個指示器功能。

首先能夠確認的是,指示器要跟ViewPager聯調,就必需要跟ViewPager的滑動狀態進行關聯。

而對於ViewPager的滑動狀態,使用的最多的就是ViewPager.OnPageChangeListener這個接口。

從圖中咱們能夠看到,微信下方的指示器滑動的時候,白點並無什麼移動動畫,而是直接就跳到另外一個點上面了,這樣一來,這個控件的實現就更加的容易了。

所以咱們能夠初步獲得思路以下:

  • 首先能夠確定的是,指示器不該該隸屬於ViewPager,不然每次instantiateItem的時候又inflate出來是很不合理的,因此咱們的indicator必須跟ViewPager同級,但能夠經過ViewPager的滑動狀態來改變。

  • 第二,小點點的數量永遠都是0~9,由於微信的圖片數量最多9張。

  • 第三,小點點都是水平居中,所以咱們的indicator能夠繼承LinearLayout來實現。

  • 第四,小點點有兩個狀態,一個選中,一個非選中。因此小點點的定製必需要提供改變選中狀態的接口。


Q1 - 代碼的編寫:

小點點的自定義

既然思路有了,那麼剩下來的也僅僅是用代碼將咱們的思路實現而已。

首先咱們來弄小點點。

因爲我懶得打開AE,因此我選擇直接採用Drawable的方式來寫。

來到drawable文件下,新建一個drawable

首先來定製一個未選中狀態的drawable

<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
    <size android:width="25dp" android:height="25dp"/>
    <stroke android:color="@color/white" android:width="1dp"/>
</shape>
複製代碼

代碼很是簡單,效果也僅僅是一個圓環。

未選中的drawable

而選中的實心圓只是把上述代碼的stroke換成solid而已,這裏就略過了。

而後咱們新建一個類繼承View,叫作**「DotView」**

或許看到繼承View你就會以爲,難道又要重寫onMeasure,onLayout什麼的?煩死了。。。。

其實不用,畢竟我們用的是drawable。。。

咱們的代碼總體結構以下:

public class DotView extends View {
    private static final String TAG = "DotView";

    //正常狀態下的dot
    Drawable mDotNormal;
    //選中狀態下的dot
    Drawable mDotSelected;

    private boolean isSelected;

    public DotView(Context context) {
        this(context, null);
    }

    public DotView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DotView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        mDotNormal = context.getResources().getDrawable(R.drawable.ic_viewpager_dot_indicator_normal);
        mDotSelected = context.getResources().getDrawable(R.drawable.ic_viewpager_dot_indicator_selected);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

    }

    public void setSelected(boolean selected) {
        this.isSelected = selected;
        invalidate();
    }

    public boolean getSelected() {
        return isSelected;
    }
}
複製代碼

能夠看到,咱們只須要實現onDraw方法和提供是否選中的方法而已。其餘的都不須要。

在onDraw裏面,咱們編寫如下代碼:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width=getWidth();
        int height=getHeight();



        if (isSelected) {
            mDotSelected.setBounds(0,0,width,height);
            mDotSelected.draw(canvas);
        }
        else {
            mDotNormal.setBounds(0,0,width,height);
            mDotNormal.draw(canvas);
        }
    }
複製代碼

這裏僅僅爲了肯定drawable的大小並根據不一樣的狀態進行不一樣的drawable繪製。很是簡單。

indicator的自定義

在上面的思路里,咱們能夠經過繼承LinearLayout來實現指示器。

所以咱們新建一個類繼承LinearLayout,取名**「DotIndicator」**

在這個指示器中,咱們須要肯定他擁有的功能:

  • 包含0~9個DotView
  • 經過公有方法來設置當前選中的DotView
  • 經過公有方法來設置當前顯示的DotView的數量

所以咱們能夠初步設計如下代碼結構:

package razerdp.friendcircle.widget;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.widget.LinearLayout;
import java.util.ArrayList;
import java.util.List;
import razerdp.friendcircle.utils.UIHelper;

/** * Created by 大燈泡 on 2016/4/21. * viewpager圖片瀏覽器底部的小點點指示器 */
public class DotIndicator extends LinearLayout {
    private static final String TAG = "DotIndicator";

    List<DotView> mDotViews;

    private int currentSelection = 0;

    private int mDotsNum = 9;

    public DotIndicator(Context context) {
        this(context,null);
    }

    public DotIndicator(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public DotIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        setOrientation(HORIZONTAL);
        setGravity(Gravity.CENTER);

        buildDotView(context);
    }

    /** * 初始化dotview * @param context */
    private void buildDotView(Context context) {

    }

    /** * 當前選中的dotview * @param selection */
    public void setCurrentSelection(int selection) {
      
    }

    public int getCurrentSelection() {
        return currentSelection;
    }

    /** * 當前須要展現的dotview數量 * @param num */
    public void setDotViewNum(int num) {
        
    }

    public int getDotViewNum() {
        return mDotsNum;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mDotViews.clear();
        mDotViews=null;
        Log.d(TAG, "清除dotview引用");
    }
}

複製代碼

在這裏說明一下,因爲咱們操做不一樣位置的dotview,因此咱們須要有一個列表來存下這些dotview。

另外,咱們設置指示器必須是水平的同時Gravity=CENTER

另外注意記得在onDetachedFromWindow清除全部引用哦。不然沒法回收就內存泄漏了。

接下來咱們補全代碼。

首先是buildDotView

在這裏咱們將會進行indicator的初始化,也就是將9個dotView添加進來

/** * 初始化dotview * @param context */
    private void buildDotView(Context context) {
        mDotViews = new ArrayList<>();
        for (int i = 0; i < 9; i++) {
            DotView dotView = new DotView(context);
            dotView.setSelected(false);
            LinearLayout.LayoutParams params = new LayoutParams(UIHelper.dipToPx(context, 10f),
                    UIHelper.dipToPx(context, 10f));
            if (i == 0) {
                params.leftMargin = 0;
            }
            else {
                params.leftMargin = UIHelper.dipToPx(context, 6f);
            }
            addView(dotView,params);
            mDotViews.add(dotView);
        }
    }
複製代碼

這裏有一個須要注意的是第0個dotview是不須要marginleft的。

接下來補全setCurrentSelection

這個方法咱們的思路也很簡單,首先將全部的DotView設置爲未選中狀態,而後再設置對應num的DotView爲選中狀態。雖然是遍歷了兩次數組,但由於不多東西,並且CPU的處理速度徹底能夠在肉眼沒法觀察的速度下完成,因此這裏無需過分考慮。

/** * 當前選中的dotview * @param selection */
    public void setCurrentSelection(int selection) {
        this.currentSelection = selection;
        for (DotView dotView : mDotViews) {
            dotView.setSelected(false);
        }
        if (selection >= 0 && selection < mDotViews.size()) {
            mDotViews.get(selection).setSelected(true);
        }
        else {
            Log.e(TAG, "the selection can not over dotViews size");
        }
    }
複製代碼

值得注意的是,咱們須要留意邊界問題

最後咱們補全setDotViewNum

這裏的思路跟上面的差很少,首先咱們將全部的dotview設置爲可見,而後將指定數量以後的dotview設置爲GONE,這時候因爲LinearLayout的Gravity是CENTER,因此剩餘的dotView會水平居中。

/** * 當前須要展現的dotview數量 * @param num */
    public void setDotViewNum(int num) {
        if (num > 9 || num <= 0) {
            Log.e(TAG, "num必須在1~9之間哦");
            return;
        }

        for (DotView dotView : mDotViews) {
            dotView.setVisibility(VISIBLE);
        }
        this.mDotsNum = num;
        for (int i = num; i < mDotViews.size(); i++) {
            DotView dotView = mDotViews.get(i);
            if (dotView != null) {
                dotView.setSelected(false);
                dotView.setVisibility(GONE);
            }
        }
    }
複製代碼

一樣須要注意邊界問題。

完成以後,咱們回到圖片瀏覽的佈局,將咱們的自定義dotindicator添加到佈局,並對其父佈局底部。

xml

最後在咱們封裝好的PhotoPagerManager引入DotIndicator

在調用showPhoto的時候,先設置dotindicator展現的dotview數量,而後再設置選中的dotview

showphoto

最後在viewpager的pagechangerlistener監聽中設置dotindicator的對應方法就行了

設置當前展現的dotview

【DotIndicator完】


Q2:viewpager複用

在上一篇文章,咱們看到當某個動態的圖片數量超過3張,咱們點擊第四張圖片的時候,會發現放大動畫並不明顯。

這是由於ViewPager的機制,ViewPager默認會緩存當前item左右共三個view,當劃到第四個,則會從新執行initItem,對應咱們的adapter,就是從新new了一個PhotoView,因爲這個PhotoView並無圖片,因此放大動畫沒法展現。

而咱們選擇解決方案就是,在adapter初始化的時候,就直接把9個photoview給new出來放到一個對象池裏面,每次執行到instantiateItem就從池裏面拿出來,這樣就能夠防止每次都new,保證放大動畫。

所以咱們的改動以下:

/** * Created by 大燈泡 on 2016/4/12. * 圖片瀏覽窗口的adapter */
public class PhotoBoswerPagerAdapter extends PagerAdapter {
    private static final String TAG = "PhotoBoswerPagerAdapter";

    private static ArrayList<MPhotoView> sMPhotoViewPool;
    private static final int sMPhotoViewPoolSize = 10;
	...跟上次同樣

    public PhotoBoswerPagerAdapter(Context context) {
    ...不變
        sMPhotoViewPool = new ArrayList<>();
        //buildProgressTV(context);
        buildMPhotoViewPool(context);
    }

    private void buildMPhotoViewPool(Context context) {
        for (int i = 0; i < sMPhotoViewPoolSize; i++) {
            MPhotoView sPhotoView = new MPhotoView(context);
            sPhotoView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
            sMPhotoViewPool.add(sPhotoView);
        }
    }

	...resetDatas()方法不變

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        MPhotoView mPhotoView = sMPhotoViewPool.get(position);
        if (mPhotoView == null) {
            mPhotoView = new MPhotoView(mContext);
            mPhotoView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
        }
        Glide.with(mContext).load(photoAddress.get(position)).into(mPhotoView);
        container.addView(mPhotoView);
        return mPhotoView;
    }
	...setPrimaryItem()方法不變

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {

        container.removeView((View) object);
    }

	...其他方法不變
    //=============================================================destroy
    public void destroy(){
        for (MPhotoView photoView : sMPhotoViewPool) {
            photoView.destroy();
        }
        sMPhotoViewPool.clear();
        sMPhotoViewPool=null;
    }
}

複製代碼

在adapter初始化的時候,咱們將對象池new出來,並new出10個photoview添加到池裏面。

在instantiateItem咱們直接從池裏面拿出來,若是沒有,才建立。而後跟之前同樣,glide載入。

在destroyItem咱們把view給remove掉,這樣能夠防止在instantiateItem的時候在池裏拿出的view擁有parent致使了異常的拋出。

最後記得提供destroy方法來清掉池的引用哦。


Q2 - 關於PhotoView在ViewPager裏面爆出的"ImageView no longer exists. You should not use this PhotoViewAttacher any more."錯誤

若是您細心,會發現個人代碼裏寫的是MPhotoView而不是PhotoView

緣由就是如小標題。

在viewpager中,若是採用對象池的方式結合PhotoView來實現複用,就會由於這個錯誤而致使PhotoView的點擊事件沒法相應。

要解決這個問題,就必須得查看PhotoView的源碼。

首先咱們找到這個錯誤的提示位置

錯誤位置

首先PhotoView的實現跟咱們PhotoPagerMananger的實現思路差很少,都是將事件的處理委託給另外一個對象,這樣的好處是能夠下降耦合度,其餘的控件想實現相似功能會更簡單。

在getImageView中,若是imageview==null,就會log出這個錯誤。

咱們看看imageview的引用,在PhotoViewAttacher中,imageview是屬於弱引用,這樣能夠更快的被回收。

而imageview的清理則是在cleanup中

/** * Clean-up the resources attached to this object. This needs to be called when the ImageView is * no longer used. A good example is from {@link android.view.View#onDetachedFromWindow()} or * from {@link android.app.Activity#onDestroy()}. This is automatically called if you are using * {@link uk.co.senab.photoview.PhotoView}. */
    @SuppressWarnings("deprecation")
    public void cleanup() {
        if (null == mImageView) {
            return; // cleanup already done
        }

        final ImageView imageView = mImageView.get();

        if (null != imageView) {
            // Remove this as a global layout listener
            ViewTreeObserver observer = imageView.getViewTreeObserver();
            if (null != observer && observer.isAlive()) {
                observer.removeGlobalOnLayoutListener(this);
            }

            // Remove the ImageView's reference to this
            imageView.setOnTouchListener(null);

            // make sure a pending fling runnable won't be run
            cancelFling();
        }

        if (null != mGestureDetector) {
            mGestureDetector.setOnDoubleTapListener(null);
        }

        // Clear listeners too
        mMatrixChangeListener = null;
        mPhotoTapListener = null;
        mViewTapListener = null;

        // Finally, clear ImageView
        mImageView = null;
    }
複製代碼

那麼如今問題的出現就很明顯了,爆出這個錯誤是由於imageview==null,也就是說兩個可能:

  • 要麼被執行了cleanup
  • 要麼就是引用的對象被銷燬了

第二點咱們能夠排除,由於咱們有個list來引用着photoview,因此只多是第一個問題。

最終,咱們在PhotoView的onDetachedFromWindow找到了cleanup方法的調用

cleanup

還記得在ViewPager中咱們的destroyItem嗎,那裏咱們執行的是container.remove(View),一個View在被remove的時候會回調onDetachedFromWindow。

而在PhotoView中,回調的時候就會執行attacher.cleanup,也就是說attacher已經沒有了imageview的引用,然而咱們的photoview倒是在咱們的池裏面。

這樣致使的結果就是在下一次instantiateItem時,從池裏拿出的photoview裏面的attacher根本就沒有imageview的引用,因此就會log出那個錯誤。

因此咱們的解決方法就很明瞭了:

把photoview的代碼copy,註釋掉onDetachedFromWindow中的mattacher.cleanup,而後提供cleanup方法來手動進行attacher.cleanup,這樣就能夠避免這個錯誤了。

大概代碼以下:

/** * Created by 大燈泡 on 2016/4/14. * * 針對onDetachedFromWindow * * 由於PhotoView在這裏會致使attacher.cleanup,從而致使attacher的imageview=null * 最終沒法在viewpager響應onPhotoViewClick * * 這裏將cleanup註釋掉,把cleanup移到手動調用方法中 */
public class MPhotoView extends ImageView implements IPhotoView {
    private PhotoViewAttacher mAttacher;

    private ScaleType mPendingScaleType;

    public MPhotoView(Context context) {
        this(context, null);
    }

    public MPhotoView(Context context, AttributeSet attr) {
        this(context, attr, 0);
    }

    public MPhotoView(Context context, AttributeSet attr, int defStyle) {
        super(context, attr, defStyle);
        super.setScaleType(ScaleType.MATRIX);
        init();
    }

    protected void init() {
        if (null == mAttacher || null == mAttacher.getImageView()) {
            mAttacher = new PhotoViewAttacher(this);
        }

        if (null != mPendingScaleType) {
            setScaleType(mPendingScaleType);
            mPendingScaleType = null;
        }
    }

...copy from photoview

    @Override
    protected void onDetachedFromWindow() {
        //mAttacher.cleanup();
        super.onDetachedFromWindow();
    }

    @Override
    protected void onAttachedToWindow() {
        init();
        super.onAttachedToWindow();
    }

    public void destroy(){
        setImageBitmap(null);
        mAttacher.cleanup();
        onDetachedFromWindow();
    }

}

複製代碼

至此,咱們上一篇留下來的問題所有解決。

下一篇。。。暫時沒想到作什麼好,你們有沒有什麼提議的

相關文章
相關標籤/搜索