專案地址:github.com/razerdp/Fri… (能弱弱的求個star或者fork麼QAQ)
【ps:評論功能羽翼君我補全了後臺互動了喲,如果您想體驗一下不同的使用者而不是一直都是羽翼君,可以在FriendCircleApp下,在onCreate中,將LocalHostInfo.INSTANCE.setHostId(1001);
的id改為1001~1115之間任意一個】
在上一篇,我們實現了朋友圈的圖片瀏覽,在文章的最後,留下了幾個問題,那麼這一片我們解決這些。
本篇需要解決的幾個問題(本篇主要為控制元件的自定義,但相信我,不會很難):
- viewpager如何複用
- 圖片瀏覽viewpager的指示器
本篇圖片預覽如下:
Q1:指示器
我們知道,在微信圖片瀏覽的時候,多張圖下方是有個指示器的,比如這樣
當然,我們可以找庫,但這個如此簡單的控制元件為此花時間去找庫,倒不如我們自己來定製一番對吧。
我們來分析一下,可以如何實現這個指示器功能。
首先可以確認的是,指示器要跟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>
複製程式碼
程式碼非常簡單,效果也僅僅是一個圓環。
而選中的實心圓只是把上述程式碼的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新增到佈局,並對其父佈局底部。
最後在我們封裝好的PhotoPagerManager引入DotIndicator
在呼叫showPhoto的時候,先設定dotindicator展示的dotview數量,然後再設定選中的dotview
最後在viewpager的pagechangerlistener監聽中設定dotindicator的對應方法就好了
【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方法的呼叫
還記得在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();
}
}
複製程式碼
至此,我們上一篇留下來的問題全部解決。
下一篇。。。暫時沒想到做什麼好,大家有沒有什麼提議的