项目地址:github.com/razerdp/Fri… (能弱弱的求个star或者fork么QAQ)java
上篇连接:一块儿撸个朋友圈吧 - 图片浏览(中)【图片浏览器】android
【ps:评论功能羽翼君我补全了后台交互了哟,若是您想体验一下不一样的用户而不是一直都是羽翼君,能够在FriendCircleApp下,在onCreate中,将LocalHostInfo.INSTANCE.setHostId(1001);
的id改成1001~1115之间任意一个】github
在上一篇,咱们实现了朋友圈的图片浏览,在文章的最后,留下了几个问题,那么这一片咱们解决这些。canvas
本篇须要解决的几个问题(本篇主要为控件的自定义,但相信我,不会很难):数组
- viewpager如何复用浏览器
- 图片浏览viewpager的指示器缓存
本篇图片预览以下:微信
咱们知道,在微信图片浏览的时候,多张图下方是有个指示器的,好比这样app
固然,咱们能够找库,但这个如此简单的控件为此花时间去找库,倒不如咱们本身来定制一番对吧。
咱们来分析一下,能够如何实现这个指示器功能。
首先能够确认的是,指示器要跟ViewPager联调,就必需要跟ViewPager的滑动状态进行关联。
而对于ViewPager的滑动状态,使用的最多的就是ViewPager.OnPageChangeListener
这个接口。
从图中咱们能够看到,微信下方的指示器滑动的时候,白点并无什么移动动画,而是直接就跳到另外一个点上面了,这样一来,这个控件的实现就更加的容易了。
所以咱们能够初步获得思路以下:
首先能够确定的是,指示器不该该隶属于ViewPager,不然每次instantiateItem的时候又inflate出来是很不合理的,因此咱们的indicator必须跟ViewPager同级,但能够经过ViewPager的滑动状态来改变。
第二,小点点的数量永远都是0~9,由于微信的图片数量最多9张。
第三,小点点都是水平居中,所以咱们的indicator能够继承LinearLayout来实现。
第四,小点点有两个状态,一个选中,一个非选中。因此小点点的定制必需要提供改变选中状态的接口。
既然思路有了,那么剩下来的也仅仅是用代码将咱们的思路实现而已。
首先咱们来弄小点点。
因为我懒得打开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>
复制代码
代码很是简单,效果也仅仅是一个圆环。
而选中的实心圆只是把上述代码的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绘制。很是简单。
在上面的思路里,咱们能够经过继承LinearLayout来实现指示器。
所以咱们新建一个类继承LinearLayout,取名**“DotIndicator”**
在这个指示器中,咱们须要肯定他拥有的功能:
所以咱们能够初步设计如下代码结构:
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添加到布局,并对其父布局底部。
最后在咱们封装好的PhotoPagerManager引入DotIndicator
在调用showPhoto的时候,先设置dotindicator展现的dotview数量,而后再设置选中的dotview
最后在viewpager的pagechangerlistener监听中设置dotindicator的对应方法就行了
【DotIndicator完】
在上一篇文章,咱们看到当某个动态的图片数量超过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方法来清掉池的引用哦。
"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,也就是说两个可能:
第二点咱们能够排除,由于咱们有个list来引用着photoview,因此只多是第一个问题。
最终,咱们在PhotoView的onDetachedFromWindow找到了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();
}
}
复制代码
至此,咱们上一篇留下来的问题所有解决。
下一篇。。。暂时没想到作什么好,你们有没有什么提议的