自定義ViewpagerIndicator (仿貓眼,添加邊緣回彈滾動效果)


一.概述

今天主要來分享個自定義viewpagerindicator,效果主要是仿 貓眼電影 頂部的欄目切換,也就是我們常說的indicator,難度簡單,為了讓滑動時效果更炫酷,我在滑動到左邊第一個item或者最右邊的item時,添加了滑動到邊緣位置后,回彈然后復位的效果(其實也是很簡單,只要計算好距離就好啦)
大致的效果圖就是這樣。大家可以湊合看看(可以看到當滑動到邊緣位置的時候有回彈的效果,是不是挺帶感的O(∩_∩)O)
這里寫圖片描述 這里寫圖片描述

二.使用方法

  1. layout布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:background="@color/red">

<mr_immortalz.com.viewpagerindicator.ViewPagerIndicator
android:id="@+id/indicator"
android:layout_width="200dp"
android:layout_height="36dp"></mr_immortalz.com.viewpagerindicator.ViewPagerIndicator>
</LinearLayout>


<android.support.v4.view.ViewPager
android:id="@+id/vp"
android:layout_width="match_parent"
android:layout_height="match_parent"></android.support.v4.view.ViewPager>
</LinearLayout>

2.MainActivity使用方法

public class MainActivity extends AppCompatActivity {
private ViewPager viewPager;
private ViewPagerIndicator indicator;
private FragmentPagerAdapter mAdapter;
private List<Fragment> mList;
private List<String> mDatas;
private int itemCount = 2;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

viewPager = (ViewPager) findViewById(R.id.vp);
indicator = (ViewPagerIndicator) findViewById(R.id.indicator);
mList = new ArrayList<Fragment>();
for (int i = 0; i < itemCount; i++) {
Fragment fragment = new MeFragment();
mList.add(fragment);
}

mDatas = new ArrayList<>();
for (int i = 0; i < itemCount; i++) {
mDatas.add("i=" + i);
}

mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int position) {
return mList.get(position);
}

@Override
public int getCount() {
return mList.size();
}
};

viewPager.setAdapter(mAdapter);
//將viewpager與indicator綁定
indicator.setDatas(mDatas);
indicator.setViewPager(viewPager);


}
}

3.自定義ViewpagerIndicator

public class ViewPagerIndicator extends LinearLayout {
private ViewPager mViewPager;

private int width;
private int height;
private int visibleItemCount = 3;
private int itemCount = 3;

//繪制框框
private Paint paint;
private float mWidth = 0;
private float mHeight = 0;
private float mLeft = 0;
private float mTop = 0;
private float radiusX = 10;
private float radiusY = 10;
private int mPadding = 8;

private List<String> mDatas;
private boolean isSetData = false;
private Context context;
private int currentPosition;
private boolean isAutoSelect = false;//判斷是否進行切換
private float rebounceOffset;

public ViewPagerIndicator(Context context) {
super(context);
this.context = context;
init();
}


public ViewPagerIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}

public ViewPagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init();

}

private void init() {
this.setBackgroundDrawable(getResources().getDrawable(R.drawable.bg));
paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(getResources().getColor(R.color.white));
paint.setAntiAlias(true);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth();
height = getMeasuredHeight();
mWidth = width / visibleItemCount;
mHeight = height;
LogUtil.m("width " + width + " height " + height);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
LogUtil.m();
super.onSizeChanged(w, h, oldw, oldh);
if (isSetData) {
isSetData = false;
this.removeAllViews();
//添加TextView
for (int i = 0; i < mDatas.size(); i++) {
TextView tv = new TextView(context);
tv.setPadding(mPadding, mPadding, mPadding, mPadding);
tv.setText(mDatas.get(i));
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
lp.width = width / visibleItemCount;
lp.height = height;
tv.setGravity(Gravity.CENTER);
tv.setTextColor(getResources().getColor(R.color.font_red));
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
tv.setLayoutParams(lp);
final int finalI = i;
tv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mViewPager != null) {
mViewPager.setCurrentItem(finalI);
}
}
});
this.addView(tv);
}
setTitleColor();
}

}

@Override
protected void onFinishInflate() {
super.onFinishInflate();
}

@Override
protected void onDraw(Canvas canvas) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//drawRoundRect需要的最低API是21
canvas.drawRoundRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, radiusX, radiusY, paint);
} else {
canvas.drawRoundRect(new RectF(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding), radiusX, radiusX, paint);
//canvas.drawRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, paint);
}


}

@Override
protected void dispatchDraw(Canvas canvas) {
//ogUtil.m();
super.dispatchDraw(canvas);
}

public void setViewPager(ViewPager viewpager, int position) {
this.mViewPager = viewpager;
this.currentPosition = position;
if (mViewPager != null) {
viewpager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//當移動的是最左邊item
if (isAutoSelect && currentPosition == 0) {
//滑動手松開時,讓最左邊(即第一個)item滑動到左邊緣位置
if (positionOffset > rebounceOffset / 2) {
mLeft = (position + (positionOffset - rebounceOffset / 2) * 2) * mWidth;
} else if (positionOffset > rebounceOffset / 3 && positionOffset < rebounceOffset / 2) {
//讓最左邊(即第一個)item 向右回彈一部分距離
mLeft = (position + (rebounceOffset / 2) - positionOffset) * mWidth * 6 / 12;
} else {
//讓最左邊(即最后一個)item 向左回彈到邊緣位置
mLeft = (position + positionOffset) * mWidth * 6 / 12;
}
invalidate();
} else if (isAutoSelect && currentPosition == itemCount - 1) {
//當移動的是最右邊(即最后一個)item

//滑動手松開時,讓最右邊(即最后一個)item滑動到右邊緣位置
if (positionOffset >= rebounceOffset && positionOffset < (1 - (1 - rebounceOffset) / 2)) {
//
mLeft = (position + positionOffset / (1 - (1 - rebounceOffset) / 2)) * mWidth;
//當item數大於visibleItem可見數,本控件(本質LinearLayout)才滾動
if (visibleItemCount < itemCount) {
scrollTo((int) (mWidth * positionOffset / (1 - (1 - rebounceOffset) / 2) + (position - visibleItemCount + 1) * mWidth), 0);
}
if ((mLeft + mWidth) > (getChildCount() * mWidth)) {
//當(mLeft + mWidth)大於最邊緣的寬度時,設置
mLeft = (itemCount - 1) * mWidth;
}
} else if (positionOffset > (1 - (1 - rebounceOffset) / 2) && positionOffset < (1 - (1 - rebounceOffset) / 4)) {
//讓最右邊(即最后一個)item 向左回彈一部分距離

//當item數大於visibleItem可見數,且本控件未滾動到指定位置,則設置控件滾動到指定位置
if (visibleItemCount < itemCount && getScrollX() != (itemCount - visibleItemCount) * mWidth) {
scrollTo((int) ((itemCount - visibleItemCount) * mWidth), 0);
}
mLeft = (position + 1) * mWidth - (positionOffset - (1 - (1 - rebounceOffset) / 2)) * mWidth * 7 / 12;
} else {
//讓最右邊(即最后一個)item 向右回彈到邊緣位置

//因為onPageScrolled 最后positionOffset會變成0,所以這里需要判斷一下
//當positionOffset = 0 時,設置mLeft位置
if (positionOffset != 0) {
mLeft = (position + 1) * mWidth - (1.0f - positionOffset) * mWidth * 7 / 12;
if (mLeft > (itemCount - 1) * mWidth) {
mLeft = (itemCount - 1) * mWidth;
}
} else {
mLeft = (itemCount - 1) * mWidth;
}

}
invalidate();
} else {
//當移動的是中間item
scrollTo(position, positionOffset);
rebounceOffset = positionOffset;
}
setTitleColor();
}

@Override
public void onPageSelected(int position) {
LogUtil.m("position " + position);
currentPosition = position;
}

@Override
public void onPageScrollStateChanged(int state) {
LogUtil.m("state " + state);
if (state == 2) {
//當state = 2時,表示手松開,viewpager自動滑動
isAutoSelect = true;
}
if (state == 0) {
//當state = 0時,表示viewpager滑動停止
isAutoSelect = false;
}
}
});
}
}


public void setViewPager(ViewPager viewpager) {
setViewPager(viewpager, 0);
}

/**
* 正常滑動
* @param position
* @param positionOffset
*/

private void scrollTo(int position, float positionOffset) {
if (visibleItemCount < itemCount) {
if (positionOffset > 0 && position > (visibleItemCount - 2)) {
this.scrollTo((int) (mWidth * positionOffset + (position - visibleItemCount + 1) * mWidth), 0);
}
}
mLeft = (position + positionOffset) * mWidth;
invalidate();
}

/**
* 設置字體顏色
*/

private void setTitleColor() {
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
if (i == currentPosition) {
((TextView) getChildAt(currentPosition)).setTextColor(getResources().getColor(R.color.font_red));
} else {
((TextView) getChildAt(i)).setTextColor(getResources().getColor(R.color.font_white));
}
}
}
}

/**
* 設置內容數據
*
* @param mDatas
*/

public void setDatas(List<String> mDatas) {
this.isSetData = true;
this.mDatas = mDatas;
this.itemCount = mDatas.size();
if (itemCount < visibleItemCount) {
visibleItemCount = itemCount;
}

}
}

三.代碼分析

很明顯,核心代碼在ViewPagerIndicator中,因為代碼中已經對每個函數方法給出了注釋,下面說下大體思路。

1.首先init(),onMeasure中對paint,width,height等必不可少的數據進行獲取。
2.因為整個indicator是繼承自linearlayout,對於里面的文字展示,用textview來顯示,因為不知道用戶使用的時候到底有多少個item,所以在setDatas()方法中對textview數目進行綁定。然后在onSizeChanged中動態生成需要的textview數目(isSetData用來控制是否綁定了數據,綁定了的話,需要將之前所有生成的全部清空)

protected void onSizeChanged(int w, int h, int oldw, int oldh) {
LogUtil.m();
super.onSizeChanged(w, h, oldw, oldh);
if (isSetData) {
isSetData = false;
this.removeAllViews();
//添加TextView
for (int i = 0; i < mDatas.size(); i++) {
TextView tv = new TextView(context);
tv.setPadding(mPadding, mPadding, mPadding, mPadding);
tv.setText(mDatas.get(i));
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
lp.width = width / visibleItemCount;
lp.height = height;
tv.setGravity(Gravity.CENTER);
tv.setTextColor(getResources().getColor(R.color.font_red));
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
tv.setLayoutParams(lp);
final int finalI = i;
tv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mViewPager != null) {
mViewPager.setCurrentItem(finalI);
}
}
});
this.addView(tv);
}
setTitleColor();
}

}

只所以在onsizechanged中動態添加,是因為該方法會在ondraw前,onMeasure方法后回調,這樣就保證我們能獲取到需要的width,height。
這里寫圖片描述
3.Ok,現在獲取到需要繪制的數目后接下來就是繪制白色背景框框啦。

protected void onDraw(Canvas canvas) {
LogUtil.m();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//drawRoundRect需要的最低API是21
canvas.drawRoundRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, radiusX, radiusY, paint);
} else {
canvas.drawRoundRect(new RectF(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding), radiusX, radiusX, paint);
//canvas.drawRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, paint);
}


}

很好理解,不解釋`(∩_∩)′
4.接下來,最最關鍵的就是setViewPager()這個方法。
為了方便理解,大家可以看看
onPageScrolled(頁面滾動時回調)
onPageSelected(滑動松手后回調,在一個滑動流程中只會回調一次)
onPageScrollStateChanged(在一個滑動流程中會回調三次,具體代表含義可以看圖中標注)
這三個方法滑動時,具體回調順序。
從第一個item向右滑動到第二個item

從第二個item滑動到第一個item(無論左滑還是右滑回調流程都一致)

知道了上面我們就應該很好理解了。

在onPageSelected中記錄currentPosition的值。
在onPageScrollStateChanged中判斷何時松開手,方便后面在松開手會對滑動進行處理
在onPageScrolled中進行滑動處理。

下面在詳細說說onPageScrolled。
onPageScrolled中也有三個判斷

1.處於最左邊item且手滑動松開
2.處於最右邊item且手滑動松開
3.其他item不管手是否滑動松開(這里用rebounceOffset記錄手松開時,已經拖動的比例positionOffset)

else {
//當移動的是中間item
scrollTo(position, positionOffset);
rebounceOffset = positionOffset;
}
private void scrollTo(int position, float positionOffset) {
//item數量大於可見item,linearlayout才滑動
if (visibleItemCount < itemCount) {
if (positionOffset > 0 && position > (visibleItemCount - 2)) {
this.scrollTo((int) (mWidth * positionOffset + (position - visibleItemCount + 1) * mWidth), 0);
}
}
mLeft = (position + positionOffset) * mWidth;
invalidate();
}

分析第一種情況。
為了實現回彈。在松手后的(positionOffset-0 ) 的時間段呢,分成三部分
看圖
這里寫圖片描述

if (isAutoSelect && currentPosition == 0) {
//滑動手松開時,讓最左邊(即第一個)item滑動到左邊緣位置
if (positionOffset > rebounceOffset / 2) {
mLeft = (position + (positionOffset - rebounceOffset / 2) * 2) * mWidth;
} else if (positionOffset > rebounceOffset / 3 && positionOffset < rebounceOffset / 2) {
//讓最左邊(即第一個)item 向右回彈一部分距離
mLeft = (position + (rebounceOffset / 2) - positionOffset) * mWidth * 6 / 12;
} else {
//讓最左邊(即最后一個)item 向左回彈到邊緣位置
mLeft = (position + positionOffset) * mWidth * 6 / 12;
}
invalidate();
}

分析第二種情況(剩余時間(positionOffset - 1 )也是分成了三部分,一部分回到邊緣,一部分偏移,一部分用於復位,與第一種情況相似,不再貼圖),當item滑向最有邊緣時,與第一種情況不同的是,Linearlayout是需要向左移動的,所以liearlayout向左移動了X,我們繪制的白色邊框需要向右移動X,才能保證,視覺上看起來白色邊框沒有動,動的是,我們的Linearlayout(不知道大家能理解不,可能我說的有點不太好理解,用紙好好繪制下簡單理解些`(∩_∩)′)

 else if (isAutoSelect && currentPosition == itemCount - 1) {
//當移動的是最右邊(即最后一個)item

//滑動手松開時,讓最右邊(即最后一個)item滑動到右邊緣位置
if (positionOffset >= rebounceOffset && positionOffset < (1 - (1 - rebounceOffset) / 2)) {
//
mLeft = (position + positionOffset / (1 - (1 - rebounceOffset) / 2)) * mWidth;
//當item數大於visibleItem可見數,本控件(本質LinearLayout)才滾動
if (visibleItemCount < itemCount) {
scrollTo((int) (mWidth * positionOffset / (1 - (1 - rebounceOffset) / 2) + (position - visibleItemCount + 1) * mWidth), 0);
}
if ((mLeft + mWidth) > (getChildCount() * mWidth)) {
//當(mLeft + mWidth)大於最邊緣的寬度時,設置
mLeft = (itemCount - 1) * mWidth;
}
} else if (positionOffset > (1 - (1 - rebounceOffset) / 2) && positionOffset < (1 - (1 - rebounceOffset) / 4)) {
//讓最右邊(即最后一個)item 向左回彈一部分距離

//當item數大於visibleItem可見數,且本控件未滾動到指定位置,則設置控件滾動到指定位置
if (visibleItemCount < itemCount && getScrollX() != (itemCount - visibleItemCount) * mWidth) {
scrollTo((int) ((itemCount - visibleItemCount) * mWidth), 0);
}
mLeft = (position + 1) * mWidth - (positionOffset - (1 - (1 - rebounceOffset) / 2)) * mWidth * 7 / 12;
}

OK,三種情況都分析完畢。最后我們的控件也算是大功告成啦`(∩_∩)′

源碼下載地址 https://github.com/ImmortalZ/ViewPagerIndicator
歡迎star,fork!`(∩_∩)′


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com