在學習這個之前,你首先要了解android的消息機制,Android的座標系統。
- scrollBy 個 scrollTo的區別
scrollTo:相對View的初始位置移動的距離。
scrollBy:相對當前位置移動的距離。
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
//mScrollX 當前的X偏移量,mScrollY 當前的Y偏移量
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
- 在實際過程中,例如我們想使控件水平向右移動200dp,那麼使用方式:
scrollTo(-200,0);
此時用戶可能會很奇怪爲什麼是-200,因爲這個偏移量是相對距離,而相對點不是屏幕左上角,而是最終View位置的左上角。
成員變量mScrollX, mScrollY,X軸方向的偏移量和Y軸方向的偏移量,這個是一個相對距離,相對的不是屏幕的左上角,而是最終View的左上角,舉個通俗易懂的例子,一列火車從吉安到深圳,途中經過贛州,那麼原點就是贛州,偏移量就是 負的吉安到贛州的距離,大家從getScrollX()方法中的註釋中就能看出答案來
Note:假如你給一個LinearLayout調用scrollTo()方法,並不是LinearLayout滾動,而是LinearLayout裏面的內容進行滾動,比如你想對一個按鈕進行滾動,直接用Button調用scrollTo()一定達不到你的需求,大家可以試一試,如果真要對某個按鈕進行scrollTo()滾動的話,我們可以在Button外面包裹一層Layout,然後對Layout調用scrollTo()方法。
- computeScrollOffset 方法
public class ScrollerLayout extends ViewGroup {
...
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
View滾動的實現原理,我們先調用Scroller的startScroll()方法來進行一些滾動的初始化設置,然後迫使View進行繪製,我們調用View的invalidate()或postInvalidate()就可以重新繪製View,繪製View的時候會觸發computeScroll()方法,我們重寫computeScroll(),在computeScroll()裏面先調用Scroller的computeScrollOffset()方法來判斷滾動有沒有結束,如果滾動沒有結束我們就調用scrollTo()方法來進行滾動,該scrollTo()方法雖然會重新繪製View,但是我們還是要手動調用下invalidate()或者postInvalidate()來觸發界面重繪,重新繪製View又觸發computeScroll(),所以就進入一個循環階段,這樣子就實現了在某個時間段裏面滾動某段距離的一個平滑的滾動效果也許有人會問,幹嘛還要調用來調用去最後在調用scrollTo()方法,還不如直接調用scrollTo()方法來實現滾動,其實直接調用是可以,只不過scrollTo()是瞬間滾動的,給人的用戶體驗不太好,所以Android提供了Scroller類實現平滑滾動的效果。爲了方面大家理解,我畫了一個簡單的scroll實現滾動的原理圖
- Scroller 使用
Scroller使用基本步驟:
- 創建Scroller的實例
- 判斷刷新時機並調用startScroll()方法來初始化滾動數據並刷新界面
- (可選)重寫computeScroll()方法,並在其內部完成平滑滾動的邏輯
package com.example.qwe;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;
public class ScrollerLayout extends ViewGroup {
//滾動實例
private Scroller mScroller;
//判定爲移動的最小像素
private int mTouchSlop;
//手機按下的時候屏幕座標
private float mXDown;
//按住移動的時候屏幕座標
private float mXMove;
//上次觸發ACTION_MOVE事件時的屏幕座標
private float mXLastMove;
//界面可滾動的左邊界
private int mLeftBorder;
//界面可滾動的右邊界
private int mRightBorder;
public ScrollerLayout(Context ctx, AttributeSet attrs){
super(ctx,attrs);
// 第一步,創建Scroller的實例
mScroller = new Scroller(ctx);
ViewConfiguration viewConfiguration = ViewConfiguration.get(ctx);
// 獲取TouchSlop值
mTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
}
//view繪製三部曲:OnMeasure,OnLayout,OnDraw
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int nChildCount = getChildCount();
for (int i = 0;i < nChildCount;++i){
View childView = getChildAt(i);
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed){
int nChildCount = getChildCount();
for (int i = 0;i < nChildCount;++i){
View childView = getChildAt(i);
//水平佈局這三個控件
childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
if(nChildCount > 0){
mLeftBorder = getChildAt(0).getLeft();
mRightBorder = getChildAt(getChildCount() - 1).getRight();
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mXDown = ev.getRawX();
mXLastMove = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
mXMove = ev.getRawX();
float diff = Math.abs(mXMove - mXDown);
//如果達到最小移動單位,則攔截ACTION_MOVE事件
if(diff > mTouchSlop)
return true;
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
mXMove = event.getRawX();
int scrolledX = (int)(mXLastMove - mXMove);
if(getScrollX() + scrolledX < mLeftBorder){
scrollTo(mLeftBorder,0);
return true;
}
else if(getScrollX() + getWidth() + scrolledX > mRightBorder){
scrollTo(mRightBorder - getWidth(),0);
return true;
}
scrollBy(scrolledX,0);
mXLastMove = mXMove;
break;
case MotionEvent.ACTION_UP:
// 當手指擡起時,根據當前的滾動值來判定應該滾動到哪個子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
//這裏爲什麼沒用使用scrollTo是爲了實現緩衝效果,而不是一下子跳躍滾動
// 第二步,調用startScroll()方法來初始化滾動數據並刷新界面
//爲了配合調用startScroll需要重寫computeScroll方法
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
super.computeScroll();
// 第三步,重寫computeScroll()方法,並在其內部完成平滑滾動的邏輯
// 返回false表示滾動完成
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<com.example.guolin.scrollertest.ScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="This is first child view"/>
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="This is second child view"/>
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="This is third child view"/>
</com.example.guolin.scrollertest.ScrollerLayout>
參考博客:
Scroller原理: https://www.jianshu.com/p/543b88fa609c
Scroller使用:https://blog.csdn.net/guolin_blog/article/details/48719871