ViewPager控件可以讓我們做出很多漂亮的界面,例如導航, 頁面菜單等. 那麼我們自己能否去實現ViewPager的效果呢? 本文將介紹如何使用ViewGroup + scrollTo + scroller實現ViewPager控件, 並且會簡單地實現一個自己的scroller, 來了解學習系統提供的scroller類滑屏功能的實現思想.
首先看一下實現的效果:
- 定義自己的ViewPager類–MyViewPager繼承ViewGroup
public class MyViewPager extends ViewGroup {
private Context context;
//手勢識別工具類
private GestureDetector detector;
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
initView();
}
private void initView() {
detector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
//處理移動事件
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//將當前視圖內容偏移(x , y)個單位,可視區域也跟着偏移(x,y)個單位
//也就是說讓視圖跟着鼠標移動, distanceX爲鼠標在屏幕上移動的距離
scrollBy((int) distanceX, 0);
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
});
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//設置每個子view的位置
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
detector.onTouchEvent(event);
return true;
}
}
(1) 在MyViewPager中, 重寫OnLayout方法, 在OnLayout方法中去確定子view在ViewPager中的位置:
通過getChildAt獲得所有的子view, 調用子view的layout方法設置每個子view的位置, layout接收4個參數(就是左上角座標與右下角座標), 來確定view的大小, 如上圖可以分析出每個view在MyViewPager中的位置, 例如第2個view的位置是:
[getWidth(), 0, 2 * getWidth(), getHeight()] , getWidth 與 getHeight爲MyViewPager的寬高. 我們可以看到變化的只有橫座標.
(2) 重寫onTouchEvent方法, 此方法處理屏幕觸摸事件, 在這裏使用用戶手勢識別工具類GestureDetector來處理action.move事件, 在OnGestureListener的onScroll中處理move事件, onScroll方法的參數distanceX, 就是工具類幫我們計算好的手指在屏幕上x軸方向移動的距離, 然後就可以很方便的使用此參數, 調用scrollBy(x, y)方法移動視圖, 讓視圖偏移(x, y). 這樣就實現了MyViewPager隨手指移動而移動.
2.使用MyViewPager. 並給MyViewPager設置子view
public class MainActivity extends AppCompatActivity {
private MyViewPager myViewPager;
private int[] imgIds = new int[] {
R.mipmap.a, R.mipmap.b, R.mipmap.c,
R.mipmap.d, R.mipmap.e, R.mipmap.f
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myViewPager = (MyViewPager) findViewById(R.id.my_viewpager);
for (int i = 0; i < imgIds.length; i++) {
ImageView imageView = new ImageView(this);
imageView.setImageResource(imgIds[i]);
myViewPager.addView(imageView);
}
}
}
佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context="com.myviewpager.MainActivity">
<com.myviewpager.MyViewPager
android:id="@+id/my_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
給MyViewPager設置6張圖片. 現在效果圖如下:
3.現在圖片可以跟隨手指滑動而滑動, 但是還不能自動地切換, 圖片只能停在你移動到的地方, 如果想實現切換的效果, 還需要我們自己去處理touch事件, 在onTouchEvent中繼續添加代碼:
//標記當前顯示在屏幕上的圖片
private int currPos = 0;
//記錄按下時的橫座標
private int firstX = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
detector.onTouchEvent(event);
//添加下面的代碼
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = getScrollX(); //獲得按下去時的橫座標
break;
case MotionEvent.ACTION_UP: //判斷顯示哪個子view, 如果滑動大於父控件的一半切換子view
int tmpPos = currPos;
if ((getScrollX() - firstX) > getWidth() / 2) { //向左滑動
tmpPos++;
} else if ((firstX - getScrollX()) > getWidth() / 2) { //向右滑動
tmpPos--;
}
MoveToDest(tmpPos); //切換到指定的圖片
break;
default:
break;
}
return true;
}
public void MoveToDest(int tmpPos) {
//確定currPos的值, 保證currPos的範圍在[0, getChildCount() - 1]
currPos = tmpPos > 0 ? tmpPos : 0;
currPos = currPos < getChildCount() - 1 ? currPos : (getChildCount() - 1);
//將視圖內容偏移至(x , y)座標處,可視區域位於(x , y)座標處
scrollTo(currPos * getWidth(), 0);
}
定義兩個成員變量, firstX, currPos來記錄點擊屏幕時的點與當前顯示在屏幕上的子view. 在touch_up事件中處理: 當在屏幕上滑動的距離大於屏幕的一半切換視圖, 否則留在當前視圖. 在MoveToDest方法中調用scrollTo來實現.
現在的效果爲:
4.在ViewPager中, 我們去切換視圖時, 並不是瞬間完成, 而是有個過程.
我們實現MyScroller類來實現滑動過程,新建MyScroller類:
public class MyScroller {
private Context context;
private int disX;
private int startY;
private int startX;
private int disY;
private long startTime; //開始動畫時間
private boolean isFinish; //標誌是否結束動畫
//默認運行時間,500ms
private int duration = 500;
//當前繪製所在的X位置
private long currX;
//當前繪製所在的Y位置
private long currY;
public MyScroller(Context context) {
this.context = context;
}
public long getCurrY() {
return currY;
}
public void setCurrY(long currY) {
this.currY = currY;
}
public long getCurrX() {
return currX;
}
public void setCurrX(long currX) {
this.currX = currX;
}
/**
* 開始移動
* @param startX 開始時的x座標
* @param startY 開始時的y座標
* @param disX x方向要移動的距離
* @param disY y方向要移動的距離
*/
public void startScroll(int startX, int startY, int disX, int disY) {
this.startX = startX;
this.startY = startY;
this.disX = disX;
this.disY = disY;
this.startTime = SystemClock.uptimeMillis();
this.isFinish = false;
}
/**
* 計算當前的運行狀況
* @return
* true 還在運行
* false 運行結束
*/
public boolean computeScrollOffset() {
if (isFinish) {
return false;
}
//獲得繪製時的時間
long passTime = SystemClock.uptimeMillis() - startTime;
//如果時間還在允許的範圍內
if (passTime <= duration) {
currX = startX + disX * passTime / duration;
currY = startY + disY * passTime / duration;
} else { //繪製運行結束
currX = startX + disX;
currY = startY + disY;
isFinish = true;
}
return true;
}
}
在startScroll方法中記錄下當前滑動點座標與目標點座標, 並記錄下當前時間. 然後在computeScrollOffset中開始去更新當前的滑動的座標.
接下來使用MyScroller來實現滑動過程,在MyViewPager類中定義MyScroller類成員變量並去使用它來實現滑動過程:
private MyScroller myScroller;
//在initView中初始化
private void initView() {
...............
myScroller = new MyScroller(context);
...............
}
然後修改moveToDest方法,不去使用ScrollTo來實現移動,使用MyScroller來實現:
public void moveToDest(int nextId) {
..................
//不使用scrollTo來實現移動
// scrollTo(currId * getWidth(), 0);
//獲得移動的距離,移動距離等於最終位置-當前位置
int distance = currId * getWidth() - getScrollX();
myScroller.startScroll(getScrollX(), 0, distance, 0);
//會導致computeScroll方法執行
invalidate();
}
@Override
public void computeScroll() {
//計算當前繪製狀況,進行繪製
if (myScroller.computeScrollOffset()) {
int nowX = (int) myScroller.getCurrX();
scrollTo(nowX, 0);
invalidate();
}
}
現在的效果你會看到視圖切換時就會有個過程而不是很快就完成了:
現在視圖的切換是勻速的,如果想達到ViewPager那種加速效果,將MyScroller改成系統提供的Scroller類即可,只需要將private MyScroller myScroller; 改爲private Scroller myScroller; 其他都不需要動就可以實現加速效果, 因爲我們的MyScroller類的接口和系統Scroller接口一樣。
5.現在MyViewPager中的View全爲ImageView, 那麼向MyViewPager中添加一個佈局(包括一個button和listView)來看一下是否同樣的支持滑動效果.
(1)佈局文件list_view.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/list_layout"
android:orientation="vertical" >
<Button
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_margin="10dp"
android:text="button"/>
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"/>
</LinearLayout>
(2)我們將佈局文件加載到MyViewPager的第四個view
private LinearLayout listLayout;
private ListView listview;
private String[] datas = { "Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango" };
@Override
protected void onCreate(Bundle savedInstanceState) {
............
//添加下面代碼
listLayout = (LinearLayout) LayoutInflater.from(getApplicationContext())
.inflate(R.layout.list_view, null);
listview = (ListView) listLayout.findViewById(R.id.listview);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
MainActivity.this, android.R.layout.simple_list_item_1, datas);
listview.setAdapter(adapter);
for (int i = 0; i < ids.length; i++) {
if (i == 3) { //將佈局文件添加到第四個位置
msv.addView(listLayout);
} else {
ImageView imageView = new ImageView(this);
imageView.setBackgroundResource(imgIds[i]);
msv.addView(imageView);
}
}
}
這個時候你運行會發現這個佈局不會顯示出來, 因爲這個時候我們沒有在MyViewPager中去調用佈局控件的OnMeasure方法, 讓其去測量子View的大小, 導致佈局view沒有顯示出來,所以我們需要在MyViewPager中去重載OnMeasure方法,去調用每個子view的measure,繪製子view的大小。
//繪製每個子控件的尺寸
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.measure(widthMeasureSpec, heightMeasureSpec);
}
}
現在的效果爲:
現在我們發現,當滑動到佈局文件中,在listview上可以上下滑,但是此時左右滑動失效了,不能左右滑動。
原因因爲Touch事件的傳遞導致, 我們知道touch事件是從父view到子view一層一層傳遞,當我們左右滑動時,此事件由MyViewPager一層層傳遞到listview,但是listview並不支持(消費)此事件,所以導致左右滑動不起作用了, 現在我們重寫onInterceptTouchEvent,來去判斷touch事件,如果是左右滑動事件那麼就去中斷此事件,不讓其再往下傳遞,我們去處理消費它。
/**
* 返回true, 中斷事件,執行自己的onTouchEvent方法
* 返回false, 默認處理,不中斷事件, 也不會執行自己的onTouchEvent方法
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean result = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//解決點擊圖片時跳動的bug
detector.onTouchEvent(ev);
firstX = (int) ev.getX();
firstY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int disx = (int) Math.abs(ev.getX() - firstX); //不管是左右移,只判斷是否左右移動
int disy = (int) Math.abs(ev.getY() - firstY); //豎直方向移動距離
//判斷是否爲水平方向移動,disx > 10 防止手指按住屏幕抖動
if(disx > disy && disx > 10) {
result = true;
} else {
result = false;
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return result;
}
現在效果如下:
此時還缺少一點就是開放一個接口讓外部來使用, 比如導航界面,點擊某一個點,會直接跳到那個點指定的頁面。
在MyViewPager中添加接口:
//監聽器對象
private MyPagerChangedListener listener;
public MyPagerChangedListener getListener() {
return listener;
}
public void setListener(MyPagerChangedListener listener) {
this.listener = listener;
}
//頁面改變時的監聽接口
public interface MyPagerChangedListener{
void moveToDest(int currId);
}
然後在moveToDest方法中去判斷此監聽器是否爲空,不爲空就調用其接口。
//移動到指定的子控件上
public void moveToDest(int nextId) {
..................
//觸發listener事件
if (listener != null) {
listener.moveToDest(currId);
}
..............
}
至此, 使用ViewGroup實現ViewPager效果完成, 如有問題可以留言。
源碼下載地址: