一块儿撸个朋友圈吧 - 图片浏览(下)【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();
    }

}

复制代码

至此,咱们上一篇留下来的问题所有解决。

下一篇。。。暂时没想到作什么好,你们有没有什么提议的