項目地址: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();
}
}
複製代碼
至此,咱們上一篇留下來的問題所有解決。
下一篇。。。暫時沒想到作什麼好,你們有沒有什麼提議的