本文導(dǎo)語:
ViewPager相信讀者們都用得很多了,在項(xiàng)目中的使用場景可以說是相當(dāng)?shù)亩嗔?,例如?br>
(1)項(xiàng)目框架的搭建,可以使用ViewPager+Fragment
(2)App引導(dǎo)頁
(3)banner輪播圖
(4)多張圖片的瀏覽等等
可能根據(jù)不同的需求,還有其他的一些使用場景,在這里就不逐一列舉了。今天就帶大家一起來手寫實(shí)現(xiàn)一下ViewPager的基本功能,不用畏懼,灰常簡單。千萬不要認(rèn)為重復(fù)造輪子是沒有意義的,可能寫了最后也是用系統(tǒng)的,但是我們的目的主要是學(xué)習(xí)其中的思想和解決問題的思路。
學(xué)習(xí)本篇文章你能收獲到:
1、自定義ViewGroup的基本流程
2、手勢識別器和Scroller的使用
3、自定義實(shí)現(xiàn)ViewPager
4、給原生ViewPager添加指示器和給自定義的ViewPager添加指示器
5、處理ViewPager中的ListView和ScrollView的滑動沖突
《一》了解一下ViewGroup和View:
1、ViewGroup相當(dāng)于一個放置View的容器,主要負(fù)責(zé)給childView計(jì)算出建議的寬高和測量模式;決定childView的位置。主要用到的方法有:
- onMesure() ——計(jì)算childView的測量值以及模式,以及設(shè)置自己的寬和高。
- onLayout()——通過getChildCount()獲取子view數(shù)量,getChildAt獲取所有子View,分別調(diào)用layout(int l, int t, int r, int b)確定每個子View的擺放位置。
- onSizeChanged——在onMeasure()后執(zhí)行,只有大小發(fā)生了變化才會執(zhí)行onSizeChange。
- onDraw——默認(rèn)不會觸發(fā),需要手動觸發(fā)。
2、View的職責(zé):是根據(jù)測量模式和ViewGroup給出的建議的寬和高,在ViewGroup為其指定的區(qū)域內(nèi)繪制出自己的形態(tài)。一般常用的方法有:
- onMesure()
- onDraw()
《二》自定義ViewPager實(shí)現(xiàn)步驟:
先看一下最終實(shí)現(xiàn)的MyViewPager的效果圖:

1、創(chuàng)建一個MyViewPager extends ViewGroup,為該自定的ViewGroup添加幾個childView。
ublic class MyViewPager extends ViewGroup {
private Context mContext;
private int[] images = {R.mipmap.bg_guide_one, R.mipmap.bg_guide_two, R.mipmap.bg_guide_three, R.mipmap.bg_guide_four};
public MyViewPager(Context context) {
super(context);
this.mContext = context;
init();
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
init();
}
public MyViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
init();
}
private void init() {
for (int i = 0; i < images.length; i++) {
ImageView iv = new ImageView(getContext());
iv.setBackgroundResource(images[i]);
this.addView(iv);
}
}
}
2、重寫onLayout()方法,獲取所有的子View,各自調(diào)用layout()方法,按下圖排列方式,確定它們各自的擺放位置。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.layout(i * getWidth(), t, (i + 1) * getWidth(), b);
}
}
3、到此,我們已經(jīng)處理好了子View的擺放位置,接下來就是處理如何讓ViewGroup中的元素,跟著手的滑動而滑動了。view可以通過onTouch事件來獲取基本的觸摸操作,但是對于較為復(fù)雜的手勢,則需要手勢識別器Gesturedetector來實(shí)現(xiàn),在此,我們使用它來處理滑動事件。
(1) 創(chuàng)建一個手勢識別器:這里主要就是靠 scrollBy()方法,來實(shí)現(xiàn)View跟隨手的滑動而滑動。
mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//相對滑動:X方向滑動多少距離,view就跟著滑動多少距離
scrollBy((int) distanceX, 0);
return super.onScroll(e1, e2, distanceX, distanceY);
}
});
(2)重寫onTouchEvent(),將觸摸事件傳遞給手勢識別器處理,并返回true,讓該控件消費(fèi)該事件。
@Override
public boolean onTouchEvent(MotionEvent event) {
//將觸摸事件傳遞手勢識別器
mGestureDetector.onTouchEvent(event);
return true;
}
到此步,運(yùn)行的效果如下:

可以看到,現(xiàn)在View已經(jīng)可以跟隨我們的手勢滑動了,但離我們預(yù)期的效果,還差兩個小問題待解決:邊界情況的處理和平滑的回彈到指定位置。
(3)邊界情況的處理。
我們期望的效果是:手指松開時,當(dāng)滑動偏移的距離超出圖片1/2時,自動切換到下個圖片;小于1/2,回彈到初始位置。這里我們需要在onTouchEvent()中處理觸摸事件,具體代碼實(shí)現(xiàn)如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
//將觸摸事件傳遞手勢識別器
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
Log.e("ACTION_MOVE", "scrollX=" + getScrollX());
scrollX = getScrollX();//相對于初始位置滑動的距離
//你滑動的距離加上屏幕的一半,除以屏幕寬度,就是當(dāng)前圖片顯示的pos.如果你滑動距離超過了屏幕的一半,這個pos就加1
position = (getScrollX() + getWidth() / 2) / getWidth();
//滑到最后一張的時候,不能出邊界
if (position >= images.length) {
position = images.length - 1;
}
if (position < 0) {
position = 0;
}
break;
case MotionEvent.ACTION_UP:
//絕對滑動,直接滑到指定的x,y的位置,較遲鈍
scrollTo(position * getWidth(), 0);
break;
}
return true;
}
這里暫時我們使用的scrollTo(int x,int y)這個方法:讓它到某個臨界值時,滑動到指定位置,由于它是讓view直接滾動到參數(shù)x和y所標(biāo)定的坐標(biāo),可以看到下面的運(yùn)行效果很遲鈍。

(4) 如何實(shí)現(xiàn)平滑的回彈到指定位置呢?這里就要用到Scroller這個類了。
Android里Scroller類是為了實(shí)現(xiàn)View平滑滾動的一個Helper類。通常在自定義的View時使用,在View中定義一個私有成員mScroller = new Scroller(context)。設(shè)置mScroller滾動的位置時,并不會導(dǎo)致View的滾動,通常是用mScroller記錄/計(jì)算View滾動的位置,再重寫View的computeScroll(),完成實(shí)際的滾動。
Scroller mScroller = new Scroller(mContext);
在onTouchEvent()中的up事件中將scrollTo()方法替換為:mScroller.startScroll();
@Override
public boolean onTouchEvent(MotionEvent event) {
//將觸摸事件傳遞手勢識別器
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
Log.e("ACTION_MOVE", "scrollX=" + getScrollX());
scrollX = getScrollX();//相對于初始位置滑動的距離
//你滑動的距離加上屏幕的一半,除以屏幕寬度,就是當(dāng)前圖片顯示的pos.如果你滑動距離超過了屏幕的一半,這個pos就加1
position = (getScrollX() + getWidth() / 2) / getWidth();
//屏蔽邊界值:postion在0~images.length-1范圍內(nèi)
if (position >= images.length) {
position = images.length - 1;
}
if (position < 0) {
position = 0;
}
break;
case MotionEvent.ACTION_UP:
//scrollTo(position * getWidth(), 0);
//滾動,startX, startY為開始滾動的位置,dx,dy為滾動的偏移量
mScroller.startScroll(scrollX, 0, -(scrollX - position * getWidth()), 0);
invalidate();//使用invalidate這個方法會有執(zhí)行一個回調(diào)方法computeScroll,我們來重寫這個方法
break;
}
}
其實(shí)Scroller的原理就是用ScrollTo()來一段一段的進(jìn)行,最后看上去跟自然的一樣,必須使用postInvalidate(),這樣才會一直回調(diào)computeScroll()這個方法,直到滑動結(jié)束。
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
Log.e("CurrX", "mScroller.getCurrX()=" + mScroller.getCurrX());
postInvalidate();
}
}
基本上ViewPager的效果就出來了,看下效果圖:

《三》給ViewPager添加指示器
看一下實(shí)現(xiàn)的效果:指示器在右下角,這里處理成了讓指示器的小點(diǎn)點(diǎn),隨著滑動的等比距離移動,當(dāng)然也可以簡單的處理成滑動到某個位置后,再移動小點(diǎn)點(diǎn),這里只是提供一個添加指示器的思路。

我們模仿系統(tǒng)的ViewPager,寫一個接口,將滑動事件的偏移距離比和當(dāng)前滑動到哪個頁面的position提供出去。
@Override
public boolean onTouchEvent(MotionEvent event) {
//將觸摸事件傳遞手勢識別器
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
Log.e("ACTION_MOVE", "scrollX=" + getScrollX());
scrollX = getScrollX();//相對于初始位置滑動的距離
//你滑動的距離加上屏幕的一半,除以屏幕寬度,就是當(dāng)前圖片顯示的pos.如果你滑動距離超過了屏幕的一半,這個pos就加1
position = (getScrollX() + getWidth() / 2) / getWidth();
//屏蔽邊界值:postion在0~images.length-1范圍內(nèi)
if (position >= images.length) {
position = images.length - 1 + 1;
}
if (position < 0) {
position = 0;
}
if (mOnPageScrollListener != null) {
Log.e("TAG", "offset=" + (float) (getScrollX() * 1.0 / ((1) * getWidth())));
mOnPageScrollListener.onPageScrolled((float) (getScrollX() * 1.0 / (getWidth())), position);
}
break;
case MotionEvent.ACTION_UP:
mScroller.startScroll(scrollX, 0, -(scrollX - position * getWidth()), 0);
invalidate();//使用invalidate這個方法會有執(zhí)行一個回調(diào)方法computeScroll,我們來重寫這個方法
if (mOnPageScrollListener != null) {
mOnPageScrollListener.onPageSelected(position);
}
break;
}
return true;
}
/**
* 其實(shí)Scroller的原理就是用ScrollTo來一段一段的進(jìn)行,最后看上去跟自然的一樣,必須使用postInvalidate,
* 這樣才會一直回調(diào)computeScroll這個方法,直到滑動結(jié)束。
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
Log.e("CurrX", "mScroller.getCurrX()=" + mScroller.getCurrX());
postInvalidate();
if (mOnPageScrollListener != null) {
Log.e("TAG", "offset=" + (float) (getScrollX() * 1.0 / (getWidth())));
mOnPageScrollListener.onPageScrolled((float) (mScroller.getCurrX() * 1.0 / ((1) * getWidth())), position);
}
}
}
public interface OnPageScrollListener {
/**
* @param offsetPercent offsetPercent:getScrollX滑動的距離占屏幕寬度的百分比
* @param position
*/
void onPageScrolled(float offsetPercent, int position);
void onPageSelected(int position);
}
private OnPageScrollListener mOnPageScrollListener;
public void setOnPageScrollListener(OnPageScrollListener onPageScrollListener) {
this.mOnPageScrollListener = onPageScrollListener;
}
}
Activity中布局中,我們在ViewPager上面放一個LinearLayout,通過addView()動態(tài)添加小點(diǎn)點(diǎn)。
public class TestActivity extends AppCompatActivity {
private MyViewPager myviewpager;
private LinearLayout llPointList;
private List<Integer> mData = new ArrayList<>();
private LinearLayout.LayoutParams params;
private View viewDot;
private int dotDistance = 30;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
myviewpager = findViewById(R.id.myviewpager);
viewDot = findViewById(R.id.view_dot);
llPointList = findViewById(R.id.ll_point_list);
initCirclePoint();
myviewpager.setOnPageScrollListener(new MyViewPager.OnPageScrollListener() {
@Override
public void onPageScrolled(float offsetPercent, int position) {
//效果一:滑動頁面過程中小圓點(diǎn)跟隨移動
//offsetPercent:0-0.5-1-1.5-...
float leftMargin = offsetPercent * dotDistance;
//如果使用系統(tǒng)的ViewPager也可以使用這種方法添加指示器,只需修改成如下即可:
//float leftMargin = positionOffset * dotDistance + position * dotDistance;
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) viewDot.getLayoutParams();
params.leftMargin = (int) leftMargin; //滑動后更新距離
// Elog.e("Offset", "params.leftMargin=" + params.leftMargin);
viewDot.setLayoutParams(params);
}
@Override
public void onPageSelected(int position) {
//效果二:滑動頁面過程中小圓點(diǎn)不跟隨移動,到某個指定位置才切換小圓點(diǎn)
Log.e("TAG", "position=" + position);
// float leftMargin = position * dotDistance;
// FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) viewDot.getLayoutParams();
// params.leftMargin = (int) leftMargin; //滑動后更新距離
//// Elog.e("Offset", "params.leftMargin=" + params.leftMargin);
// viewDot.setLayoutParams(params);
}
});
}
private void initCirclePoint() {
for (int i = 0; i < 4; i++) {
mData.add(i);
}
for (int i = 0; i < mData.size(); i++) {
View point = new View(this);
point.setBackgroundResource(R.drawable.bg_point_selector);
params = new LinearLayout.LayoutParams(20, 20);
if (i != 0) {
params.leftMargin = 10;
}
point.setEnabled(false);
point.setLayoutParams(params);
llPointList.addView(point);
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.jojo.learn.customview.MyViewPager
android:id="@+id/myviewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"></com.example.jojo.learn.customview.MyViewPager>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="64px"
android:layout_marginRight="30px">
<LinearLayout
android:id="@+id/ll_point_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp"
android:layout_marginRight="40dp"
android:orientation="horizontal"></LinearLayout>
<View
android:id="@+id/view_dot"
android:layout_width="20px"
android:layout_height="20px"
android:background="@drawable/bg_shape_white_point"></View>
</FrameLayout>
</RelativeLayout>
bg_shape_white_point.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/color_a9c6d6"></solid>
</shape>
bg_shape_grey_point.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/color_918f8e"></solid>
</shape>
bg_point_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg_shape_white_point" android:state_enabled="true"/>
<item android:drawable="@drawable/bg_shape_grey_point" android:state_enabled="false"/>
</selector>
《四》滑動沖突的處理(在這里以ScrollView為例講解滑動沖突,ListView的處理方式跟其一樣)
往MyViewPager添加一個ScrollView的子view,看如下圖,會發(fā)現(xiàn)上下能滑動,左右滑動失效了。

滑動沖突原因分析:
MyViewPager是左右滑動,子View(ScrollView)是上下滑動。事件傳遞的過程中,如果父View無攔截?zé)o消耗,那么當(dāng)事件傳遞到子View時,默認(rèn)會被子View(ScrollView)消費(fèi),那么事件在ScrollView中就傳遞結(jié)束了,所以父View(MyViewPager)的左右滑動就失效了。
解決沖突的辦法:
就是重寫父View的onInterceptTouchEvent()事件,在合適的時候,攔截該事件。
onInterceptTouchEvent()方法返回值的含義:
1、 如果return true,則表示將事件進(jìn)行攔截,并將攔截到的事件交由當(dāng)前 View 的 onTouchEvent 進(jìn)行處理;
2、 如果return false,則表示將事件放行,當(dāng)前 View 上的事件會被傳遞到子 View 上,再由子 View 的 dispatchTouchEvent 來開始這個事件的分發(fā);
根據(jù)我們的期望的效果:左右滑動時,讓父View消費(fèi)該事件;上下滑動時,直接放行,讓子View(ScrollView)自己處理。代碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 如果左右滑動, 就需要攔截, 上下滑動,不需要攔截
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) ev.getX();
startY = (int) ev.getY();
//這個時候還需要把將ACTION_DOWN傳遞給手勢識別器,因?yàn)閿r截了MOVE的事件后,DOWN的事件還是要給手勢識別器處理,否則會丟失事件,滑動的時候會存在bug。
mGestureDetector.onTouchEvent(ev);
break;
case MotionEvent.ACTION_MOVE:
int endX = (int) ev.getX();
int endY = (int) ev.getY();
int dx = endX - startX;
int dy = endY - startY;
if (Math.abs(dx) > Math.abs(dy)) {
// 左右滑動
return true;// 中斷事件傳遞, 不允許孩子響應(yīng)事件了, 由父控件處理
}
break;
default:
break;
}
return false;// 不攔截事件,優(yōu)先傳遞給孩子(也就是ScrollView,讓它正常處理上下滑動事件)處理
}
《五》MyViewPager完整代碼:
/**
* Created by JoJo on 2018/8/14.
* wechat:18510829974
* description:自定義ViewPager
*/
public class MyViewPager extends ViewGroup {
private Context mContext;
private int[] images = {R.mipmap.bg_subject_detail_default, R.mipmap.bg_subject_default, R.mipmap.bg_guide_one, R.mipmap.bg_guide_two};
private GestureDetector mGestureDetector;
private Scroller mScroller;
private int position;
private int scrollX;
private int startX;
private int startY;
public MyViewPager(Context context) {
super(context);
this.mContext = context;
init();
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
init();
}
public MyViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
init();
}
private void init() {
for (int i = 0; i < images.length; i++) {
ImageView iv = new ImageView(getContext());
iv.setBackgroundResource(images[i]);
this.addView(iv);
}
//由于ViewGroup默認(rèn)只測量下面一層的子View(所以我們直接在ViewGroup里面添加ImageView是可以直接顯示出來的),所以基本自定義ViewGroup都會要重寫onMeasure方法,否則無法測量第一層View(這里是ScrollView)中的view,無法正常顯示里面的內(nèi)容。
View testView = View.inflate(mContext, R.layout.test_viewpager_scrollview, null);
addView(testView, 2);
mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//相對滑動:X方向滑動多少距離,view就跟著滑動多少距離
scrollBy((int) distanceX, 0);
return super.onScroll(e1, e2, distanceX, distanceY);
}
});
mScroller = new Scroller(mContext);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
//如果是view:觸發(fā)view的測量;如果是ViewGroup,觸發(fā)測量ViewGroup中的子view
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 如果左右滑動, 就需要攔截, 上下滑動,不需要攔截
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) ev.getX();
startY = (int) ev.getY();
mGestureDetector.onTouchEvent(ev);
break;
case MotionEvent.ACTION_MOVE:
int endX = (int) ev.getX();
int endY = (int) ev.getY();
int dx = endX - startX;
int dy = endY - startY;
if (Math.abs(dx) > Math.abs(dy)) {
// 左右滑動
return true;// 中斷事件傳遞, 不允許孩子響應(yīng)事件了, 由父控件處理
}
break;
default:
break;
}
return false;// 不攔截事件,優(yōu)先傳遞給孩子(也就是ScrollView,讓它正常處理上下滑動事件)處理
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//將觸摸事件傳遞手勢識別器
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
Log.e("ACTION_MOVE", "scrollX=" + getScrollX());
scrollX = getScrollX();//相對于初始位置滑動的距離
//你滑動的距離加上屏幕的一半,除以屏幕寬度,就是當(dāng)前圖片顯示的pos.如果你滑動距離超過了屏幕的一半,這個pos就加1
position = (getScrollX() + getWidth() / 2) / getWidth();
//屏蔽邊界值:postion在0~images.length-1范圍內(nèi)
if (position >= images.length) {
position = images.length - 1 + 1;
}
if (position < 0) {
position = 0;
}
if (mOnPageScrollListener != null) {
Log.e("TAG", "offset=" + (float) (getScrollX() * 1.0 / ((1) * getWidth())));
mOnPageScrollListener.onPageScrolled((float) (getScrollX() * 1.0 / (getWidth())), position);
}
break;
case MotionEvent.ACTION_UP:
//絕對滑動,直接滑到指定的x,y的位置,較遲鈍
// scrollTo(position * getWidth(), 0);
// Log.e("TAG", "水平方向回彈滑動的距離=" + (-(scrollX - position * getWidth())));
//滾動,startX, startY為開始滾動的位置,dx,dy為滾動的偏移量
mScroller.startScroll(scrollX, 0, -(scrollX - position * getWidth()), 0);
invalidate();//使用invalidate這個方法會有執(zhí)行一個回調(diào)方法computeScroll,我們來重寫這個方法
if (mOnPageScrollListener != null) {
mOnPageScrollListener.onPageSelected(position);
}
break;
}
return true;
}
/**
* 其實(shí)Scroller的原理就是用ScrollTo來一段一段的進(jìn)行,最后看上去跟自然的一樣,必須使用postInvalidate,
* 這樣才會一直回調(diào)computeScroll這個方法,直到滑動結(jié)束。
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
Log.e("CurrX", "mScroller.getCurrX()=" + mScroller.getCurrX());
postInvalidate();
if (mOnPageScrollListener != null) {
Log.e("TAG", "offset=" + (float) (getScrollX() * 1.0 / (getWidth())));
mOnPageScrollListener.onPageScrolled((float) (mScroller.getCurrX() * 1.0 / ((1) * getWidth())), position);
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.layout(i * getWidth(), t, (i + 1) * getWidth(), b);
}
}
public interface OnPageScrollListener {
/**
* @param offsetPercent offsetPercent:getScrollX滑動的距離占屏幕寬度的百分比
* @param position
*/
void onPageScrolled(float offsetPercent, int position);
void onPageSelected(int position);
}
private OnPageScrollListener mOnPageScrollListener;
public void setOnPageScrollListener(OnPageScrollListener onPageScrollListener) {
this.mOnPageScrollListener = onPageScrollListener;
}
}
存在待解決的問題:
1、當(dāng)快速切換時,頁面會無法切換。有興趣朋友的歡迎一起交流學(xué)習(xí)。
本文參考學(xué)習(xí):
Android 手把手教您自定義ViewGroup
Android手勢檢測——GestureDetector全面分析
觸摸事件的處理和傳遞dispatchTouchEvent、onInterceptTouchEvent
Android分析View的scrollBy()和scrollTo()的參數(shù)正負(fù)問題原理分析
scrollTo/scrollBy 使用詳解
最后,附上我的一個Kotlin編寫+組件化開發(fā)的開源項(xiàng)目Designer
Kotlin+組件化開發(fā)實(shí)踐—開源項(xiàng)目Designer-App
Designer項(xiàng)目算是傾注了我蠻多心血了,每個頁面和功能都當(dāng)成是上線的App來做,App的logo還特地做了UI設(shè)計(jì)??力求做到精致和完善,其中還包括了很多自己項(xiàng)目開發(fā)中的經(jīng)驗(yàn)匯總和對新技術(shù)的探索和整合,希望對各位讀者有所幫助,歡迎點(diǎn)個star,follow,或者給個小心心,嘻嘻??也可以分享給你更多的朋友一起學(xué)習(xí),您的支持是我不斷前進(jìn)的動力。如果有任何問題,歡迎在GitHub上給我提issue或者留言。