項(xiàng)目地址:https://github.com/razerdp/FriendCircle (能弱弱的求個(gè)star或者fork么QAQ)
《一起擼個(gè)朋友圈吧》 這是本文所處文集,所有更新都會(huì)在這個(gè)文集里面哦挂谍,歡迎關(guān)注
上篇鏈接:http://www.reibang.com/p/4c5b5d7dc856
下篇鏈接:http://www.reibang.com/p/42119b89c26f
【ps:評(píng)論功能羽翼君我補(bǔ)全了后臺(tái)交互了喲漩绵,如果您想體驗(yàn)一下不同的用戶而不是一直都是羽翼君匆赃,可以在FriendCircleApp下熟嫩,在onCreate中,將LocalHostInfo.INSTANCE.setHostId(1001);
的id改為1001~1115之間任意一個(gè)】
在上一篇夏哭,我們實(shí)現(xiàn)了朋友圈的圖片瀏覽满哪,在文章的最后,留下了幾個(gè)問(wèn)題汛兜,那么這一片我們解決這些巴粪。
本篇需要解決的幾個(gè)問(wèn)題(本篇主要為控件的自定義,但相信我粥谬,不會(huì)很難):
- viewpager如何復(fù)用
- 圖片瀏覽viewpager的指示器
本篇圖片預(yù)覽如下:
Q1:指示器
我們知道肛根,在微信圖片瀏覽的時(shí)候,多張圖下方是有個(gè)指示器的漏策,比如這樣
當(dāng)然派哲,我們可以找?guī)欤@個(gè)如此簡(jiǎn)單的控件為此花時(shí)間去找?guī)煊寸瑁共蝗缥覀冏约簛?lái)定制一番對(duì)吧狮辽。
我們來(lái)分析一下,可以如何實(shí)現(xiàn)這個(gè)指示器功能巢寡。
首先可以確認(rèn)的是喉脖,指示器要跟ViewPager聯(lián)調(diào),就必須要跟ViewPager的滑動(dòng)狀態(tài)進(jìn)行關(guān)聯(lián)抑月。
而對(duì)于ViewPager的滑動(dòng)狀態(tài)树叽,使用的最多的就是ViewPager.OnPageChangeListener
這個(gè)接口。
從圖中我們可以看到谦絮,微信下方的指示器滑動(dòng)的時(shí)候题诵,白點(diǎn)并沒(méi)有什么移動(dòng)動(dòng)畫,而是直接就跳到另一個(gè)點(diǎn)上面了层皱,這樣一來(lái)性锭,這個(gè)控件的實(shí)現(xiàn)就更加的容易了。
因此我們可以初步得到思路如下:
首先可以肯定的是叫胖,指示器不應(yīng)該隸屬于ViewPager草冈,否則每次instantiateItem的時(shí)候又inflate出來(lái)是很不合理的,所以我們的indicator必須跟ViewPager同級(jí)瓮增,但可以通過(guò)ViewPager的滑動(dòng)狀態(tài)來(lái)改變怎棱。
第二,小點(diǎn)點(diǎn)的數(shù)量永遠(yuǎn)都是0~9绷跑,因?yàn)槲⑿诺膱D片數(shù)量最多9張拳恋。
第三,小點(diǎn)點(diǎn)都是水平居中砸捏,因此我們的indicator可以繼承LinearLayout來(lái)實(shí)現(xiàn)谬运。
第四隙赁,小點(diǎn)點(diǎn)有兩個(gè)狀態(tài),一個(gè)選中梆暖,一個(gè)非選中鸳谜。所以小點(diǎn)點(diǎn)的定制必須要提供改變選中狀態(tài)的接口。
Q1 - 代碼的編寫:
小點(diǎn)點(diǎn)的自定義
既然思路有了式廷,那么剩下來(lái)的也僅僅是用代碼將我們的思路實(shí)現(xiàn)而已咐扭。
首先我們來(lái)弄小點(diǎn)點(diǎn)。
由于我懶得打開(kāi)AE滑废,所以我選擇直接采用Drawable的方式來(lái)寫蝗肪。
來(lái)到drawable文件下,新建一個(gè)drawable
首先來(lái)定制一個(gè)未選中狀態(tài)的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>
代碼非常簡(jiǎn)單蠕趁,效果也僅僅是一個(gè)圓環(huán)薛闪。
而選中的實(shí)心圓只是把上述代碼的stroke換成solid而已,這里就略過(guò)了俺陋。
然后我們新建一個(gè)類繼承View豁延,叫做“DotView”
或許看到繼承View你就會(huì)覺(jué)得,難道又要重寫onMeasure,onLayout什么的腊状?煩死了诱咏。。缴挖。袋狞。
其實(shí)不用,畢竟咱們用的是drawable映屋。苟鸯。。
我們的代碼整體結(jié)構(gòu)如下:
public class DotView extends View {
private static final String TAG = "DotView";
//正常狀態(tài)下的dot
Drawable mDotNormal;
//選中狀態(tài)下的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;
}
}
可以看到棚点,我們只需要實(shí)現(xiàn)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的大小并根據(jù)不同的狀態(tài)進(jìn)行不同的drawable繪制砌梆。非常簡(jiǎn)單。
indicator的自定義
在上面的思路里颁股,我們可以通過(guò)繼承LinearLayout來(lái)實(shí)現(xiàn)指示器么库。
因此我們新建一個(gè)類繼承LinearLayout傻丝,取名“DotIndicator”
在這個(gè)指示器中甘有,我們需要確定他擁有的功能:
- 包含0~9個(gè)DotView
- 通過(guò)公有方法來(lái)設(shè)置當(dāng)前選中的DotView
- 通過(guò)公有方法來(lái)設(shè)置當(dāng)前顯示的DotView的數(shù)量
因此我們可以初步設(shè)計(jì)以下代碼結(jié)構(gòu):
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圖片瀏覽器底部的小點(diǎn)點(diǎn)指示器
*/
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) {
}
/**
* 當(dāng)前選中的dotview
* @param selection
*/
public void setCurrentSelection(int selection) {
}
public int getCurrentSelection() {
return currentSelection;
}
/**
* 當(dāng)前需要展示的dotview數(shù)量
* @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引用");
}
}
在這里說(shuō)明一下,由于我們操作不同位置的dotview葡缰,所以我們需要有一個(gè)列表來(lái)存下這些dotview亏掀。
另外忱反,我們?cè)O(shè)置指示器必須是水平的同時(shí)Gravity=CENTER
另外注意記得在onDetachedFromWindow清除所有引用哦。否則無(wú)法回收就內(nèi)存泄漏了滤愕。
接下來(lái)我們補(bǔ)全代碼温算。
首先是
buildDotView
在這里我們將會(huì)進(jìn)行indicator的初始化,也就是將9個(gè)dotView添加進(jìn)來(lái)
/**
* 初始化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);
}
}
這里有一個(gè)需要注意的是第0個(gè)dotview是不需要marginleft的间影。
接下來(lái)補(bǔ)全
setCurrentSelection
這個(gè)方法我們的思路也很簡(jiǎn)單注竿,首先將所有的DotView設(shè)置為未選中狀態(tài),然后再設(shè)置對(duì)應(yīng)num的DotView為選中狀態(tài)魂贬。雖然是遍歷了兩次數(shù)組巩割,但因?yàn)楹苌贃|西,而且CPU的處理速度完全可以在肉眼無(wú)法觀察的速度下完成付燥,所以這里無(wú)需過(guò)度考慮宣谈。
/**
* 當(dāng)前選中的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");
}
}
值得注意的是,我們需要留意邊界問(wèn)題
最后我們補(bǔ)全setDotViewNum
這里的思路跟上面的差不多键科,首先我們將所有的dotview設(shè)置為可見(jiàn)闻丑,然后將指定數(shù)量之后的dotview設(shè)置為GONE,這時(shí)候由于LinearLayout的Gravity是CENTER勋颖,所以剩余的dotView會(huì)水平居中嗦嗡。
/**
* 當(dāng)前需要展示的dotview數(shù)量
* @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);
}
}
}
同樣需要注意邊界問(wèn)題。
完成之后饭玲,我們回到圖片瀏覽的布局酸钦,將我們的自定義dotindicator添加到布局,并對(duì)其父布局底部咱枉。
最后在我們封裝好的PhotoPagerManager引入DotIndicator
在調(diào)用showPhoto的時(shí)候卑硫,先設(shè)置dotindicator展示的dotview數(shù)量,然后再設(shè)置選中的dotview
最后在viewpager的pagechangerlistener監(jiān)聽(tīng)中設(shè)置dotindicator的對(duì)應(yīng)方法就好了
【DotIndicator完】
Q2:viewpager復(fù)用
在上一篇文章蚕断,我們看到當(dāng)某個(gè)動(dòng)態(tài)的圖片數(shù)量超過(guò)3張欢伏,我們點(diǎn)擊第四張圖片的時(shí)候,會(huì)發(fā)現(xiàn)放大動(dòng)畫并不明顯亿乳。
這是因?yàn)閂iewPager的機(jī)制硝拧,ViewPager默認(rèn)會(huì)緩存當(dāng)前item左右共三個(gè)view,當(dāng)劃到第四個(gè)葛假,則會(huì)重新執(zhí)行initItem障陶,對(duì)應(yīng)我們的adapter,就是重新new了一個(gè)PhotoView聊训,由于這個(gè)PhotoView并沒(méi)有圖片抱究,所以放大動(dòng)畫無(wú)法展示。
而我們選擇解決方案就是带斑,在adapter初始化的時(shí)候鼓寺,就直接把9個(gè)photoview給new出來(lái)放到一個(gè)對(duì)象池里面勋拟,每次執(zhí)行到instantiateItem就從池里面拿出來(lái),這樣就可以防止每次都new妈候,保證放大動(dòng)畫敢靡。
因此我們的改動(dòng)如下:
/**
* 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初始化的時(shí)候,我們將對(duì)象池new出來(lái)苦银,并new出10個(gè)photoview添加到池里面啸胧。
在instantiateItem我們直接從池里面拿出來(lái),如果沒(méi)有幔虏,才創(chuàng)建吓揪。然后跟以前一樣,glide載入所计。
在destroyItem我們把view給remove掉柠辞,這樣可以防止在instantiateItem的時(shí)候在池里拿出的view擁有parent導(dǎo)致了異常的拋出。
最后記得提供destroy方法來(lái)清掉池的引用哦主胧。
Q2 - 關(guān)于PhotoView在ViewPager里面爆出的"ImageView no longer exists. You should not use this PhotoViewAttacher any more."錯(cuò)誤
如果您細(xì)心叭首,會(huì)發(fā)現(xiàn)我的代碼里寫的是MPhotoView而不是PhotoView
原因就是如小標(biāo)題。
在viewpager中踪栋,如果采用對(duì)象池的方式結(jié)合PhotoView來(lái)實(shí)現(xiàn)復(fù)用焙格,就會(huì)因?yàn)檫@個(gè)錯(cuò)誤而導(dǎo)致PhotoView的點(diǎn)擊事件無(wú)法相應(yīng)。
要解決這個(gè)問(wèn)題夷都,就必須得查看PhotoView的源碼眷唉。
首先我們找到這個(gè)錯(cuò)誤的提示位置
首先PhotoView的實(shí)現(xiàn)跟我們PhotoPagerMananger的實(shí)現(xiàn)思路差不多,都是將事件的處理委托給另一個(gè)對(duì)象囤官,這樣的好處是可以降低耦合度冬阳,其他的控件想實(shí)現(xiàn)類似功能會(huì)更簡(jiǎn)單。
在getImageView中党饮,如果imageview==null肝陪,就會(huì)log出這個(gè)錯(cuò)誤。
我們看看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;
}
那么現(xiàn)在問(wèn)題的出現(xiàn)就很明顯了狼讨,爆出這個(gè)錯(cuò)誤是因?yàn)閕mageview==null,也就是說(shuō)兩個(gè)可能:
- 要么被執(zhí)行了cleanup
- 要么就是引用的對(duì)象被銷毀了
第二點(diǎn)我們可以排除柒竞,因?yàn)槲覀冇袀€(gè)list來(lái)引用著photoview政供,所以只可能是第一個(gè)問(wèn)題。
最終,我們?cè)赑hotoView的onDetachedFromWindow找到了cleanup方法的調(diào)用
還記得在ViewPager中我們的destroyItem嗎鲫骗,那里我們執(zhí)行的是container.remove(View),一個(gè)View在被remove的時(shí)候會(huì)回調(diào)onDetachedFromWindow踩晶。
而在PhotoView中执泰,回調(diào)的時(shí)候就會(huì)執(zhí)行attacher.cleanup,也就是說(shuō)attacher已經(jīng)沒(méi)有了imageview的引用渡蜻,然而我們的photoview卻是在我們的池里面术吝。
這樣導(dǎo)致的結(jié)果就是在下一次instantiateItem時(shí),從池里拿出的photoview里面的attacher根本就沒(méi)有imageview的引用茸苇,所以就會(huì)log出那個(gè)錯(cuò)誤排苍。
所以我們的解決方法就很明了了:
把photoview的代碼copy,注釋掉onDetachedFromWindow中的mattacher.cleanup学密,然后提供cleanup方法來(lái)手動(dòng)進(jìn)行attacher.cleanup淘衙,這樣就可以避免這個(gè)錯(cuò)誤了。
大概代碼如下:
/**
* Created by 大燈泡 on 2016/4/14.
*
* 針對(duì)onDetachedFromWindow
*
* 因?yàn)镻hotoView在這里會(huì)導(dǎo)致attacher.cleanup腻暮,從而導(dǎo)致attacher的imageview=null
* 最終無(wú)法在viewpager響應(yīng)onPhotoViewClick
*
* 這里將cleanup注釋掉彤守,把cleanup移到手動(dòng)調(diào)用方法中
*/
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();
}
}
至此,我們上一篇留下來(lái)的問(wèn)題全部解決哭靖。
下一篇具垫。。试幽。暫時(shí)沒(méi)想到做什么好筝蚕,大家有沒(méi)有什么提議的