View的滑動
- 使用scrollTo/scrollBy
- 使用動畫
- 改變佈局參數
- 各種滑動方式的對比
彈性滑動
- 使用Scroller
- 通過動畫‘
- 使用延時策略
在上一節介紹了View的一些基礎知識和概念,本節開始介紹很重要的一個內容:View的滑動。在Android設備上,滑動幾乎是應用的標配,不管是下拉刷新還是SlidingMenu,它們的基礎都是滑動。從另外一方面來說,Android手機由於屏幕比較小,爲了給用戶呈現更多的內容,就需要使用滑動來隱藏和顯示一些內容。基於上述兩點,可以知道,滑動在Android開發中具有很重要的作用,不管一些滑動效果多麼絢麗,歸根結底,它們都是由不同的滑動外加一些特效所組成的。因此,掌握滑動的方法是實現絢麗的自定義控件的基礎。
通過三種方式可以實現View的滑動:
第一種是通過View本身提供的scrollTo/scrollBy方法來實現滑動;
第二種是通過動畫給View施加平移效果來實現滑動;
第三種是通過改變Viev的LayoutParams使得View重新佈局從而實現滑動。
從目前來看,常見的滑動方式就這麼三種,下面一一進行分析。
1.使用scrollTo/scrollBy
爲了實現View的滑動,View提供了專門的方法來實現這個功能,那就是scrollTo/scrollBy,我們先來看看這兩個方法的實現,如下所示。
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
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();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
2.使用動畫
上一節介紹了採用scrollTo/scrollBy來實現View的滑動,本節課介紹另外一種滑動方式,就是使用動畫,通過動畫,我們來讓一個View移動,而平移就是一種滑動,使用動畫來移動View,主要是操作View的translationX,translationY屬性,即可以採用傳統的View動畫,也可以採用屬性動畫,如果用屬性動畫的話,爲了兼容3.0以下的版本需要使用開源庫nineoldandroids(github上自行搜索)
採用View動畫的代碼,如下所示,此動畫可以在100ms裏讓一個View從初始的位置向右下角移動100個像素
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">
<translate
android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:interpolator="@android:anim/linear_interpolator"
android:toXDelta="100"
android:toYDelta="100"
/>
</set>
如果採用屬性動畫的話,那就更簡單了,我們可用這樣
ObjectAnimator.ofFloat(testButton,"translationX",0,100).setDuration(100).start();
3.改變佈局參數
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) mButton.getLayoutParams();
layoutParams.width +=100;
layoutParams.leftMargin +=100;
mButton.requestLayout();
//或者mButton.setLayoutParams(layoutParams);
通過改變LayoutParams的方式去實現View的滑動同樣是一種很靈活的方法,需要根據不同情況去做不同的處理。
4.各種滑動方式的對比
上面分別介紹了三種不同的滑動方式,它們都能實現View的滑動,那麼它們之間的差別分別是什麼呢?
先看scorllBy/To這種方式,他是View提供的原生方式,其作用是專門用於View的滑動,它可以比較方便地實現滑動效果並且不影響內部元素的單擊事件。但是它的缺點也是很顯然的:它只能滑動View的內容,並不能滑動View本身。
再看動畫,通過動畫來實現View的滑動,這要分情況。如果是Android3.0以上並採用屬性動畫,那麼採用這種方式沒有明顯的缺點;如果是使用View動畫或者在Android3.0以下使用屬性動畫,均不能改變View本身的屬性。在實際使用中,如果動畫元素不需要響應用戶的交互,那麼使用動畫來做滑動是比較合適的,否則就不太適合**。但是動畫有一很明顯的優點,那就是一些複雜的效果必須要通過動畫才能實現,**
改變佈局方式,主要適用對象是一些具有交互性的View,因爲這些View需要和用戶交互,直接通過動畫去實現會有問題,這在之前已經有所介紹,所以這個時候我們可以使用直接改變佈局參數的方式去實現:
scrollTo/scrollBy:操作簡單,適合對View內容的滑動:
動畫:操作簡單,主要適用於沒有交互的Visw和實現複雜的動畫效果
改變佈局參數:操作稍微複雜,適用於有交互的View
下面我們來實現一個手滑動的效果,這是一個自定義的View,拖動他可以讓他在整個屏幕上隨意滑動,這個View實現起來很簡單,我們只要重寫他的onTouchEvent方法並且處理他的ACTION_MOVE事件,根據兩次滑動之間的距離就可以實現它的滑動,爲了實現全屏滑動,我們採用改變佈局的方式來實現,這裏只是演示,所以就選擇了動畫的方式,核心代碼:
package com.example.testgesturedetector;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
import com.nineoldandroids.view.ViewHelper;
public class MyViewMove extends LinearLayout {
private static final String TAG = "MyViewMove";
public MyViewMove(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public MyViewMove(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MyViewMove(Context context) {
super(context);
init(context);
}
private void init(Context context) {
}
int mLastX = 0;
int mLastY = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
//這裏這個ViewHelper是nineoldandroid包裏面的工具類,需要在build.gradle中添加
//implementation "com.nineoldandroids:library:2.4.0"
int trabslationX = (int) (ViewHelper.getTranslationX(this) + deltaX);
int trabslationY = (int) (ViewHelper.getTranslationY(this) + deltaY);
ViewHelper.setTranslationX(this,trabslationX);
ViewHelper.setTranslationY(this,trabslationY);
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return true;
}
}
xml增加
<com.example.testgesturedetector.MyViewMove
android:layout_width="50dp"
android:layout_height="50dp"
android:background="#ff0000"
/>
實現的效果就是我們可以拖動着這個紅色的方塊滿屏幕跑(實際測試通過)
彈性滑動
知道了View的滑動,我們還要知道如何實現View的彈性滑動,比較生硬地滑動過去這種用戶體驗實在是太差了,因此我們要實現漸進式滑動,那麼如何實現彈性滑動呢?其實實現方法也是有很多,但是他們都有一個共同的思想:將一次大的滑動分成若干個小的滑動,並且在一個時間段完成,實現方式很多,比如Scroller,Handler#PostDelayed以及Thread#Sleep,我們接下來一一介紹:
1.Scroller
Scroller的使用方法在之前就已經介紹了,我們來分析一下他的源碼,從而探索爲什麼能實現View的彈性滑動
Scroller scroller = new Scroller(getContext());
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
//1000ms內滑向destX,效果就是慢慢的滑動
scroller.startScroll(scrollX,0,delta,0,1000);
invalidate();
}
@Override
public void computeScroll() {
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
上面是Scroller的典型用法,這裏先描述一下他的工作原理,當我們構建一個scroller對象並且調用它的startScroll方法,scroller內部其實並沒有做什麼,他只是保存了我們傳遞的參數,這幾個參數從startScroll的原型就可以看出,如下的代碼:
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
我們再來看下Scroller的computeScrollOffset方法的實現:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
...
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
我們寫個demo來測試下
實現的功能就是,點擊view會進行scroller滑動(黑色的區域,綠色的滑塊移動),因爲是view的內容移動,所以只會移動xml中的內容,就是綠色滑塊。
核心代碼:
package com.example.testgesturedetector;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
import android.widget.Scroller;
public class MyViewScroller extends LinearLayout {
private Scroller mScroller;
private static final String TAG = "MyView";
public MyViewScroller(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public MyViewScroller(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MyViewScroller(Context context) {
super(context);
init(context);
}
private void init(Context context) {
Log.d(TAG,"步驟1:初始化一個Scroller。");
mScroller = new Scroller(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
Log.d(TAG,"點擊自定義View,向右移動50。");
smoothScrollTo(-50,0);
break;
}
Log.d(TAG,"一定要返回true才生效。");
return true;
}
public void smoothScrollTo(int destX,int destY){
Log.d(TAG,"步驟2:定義一個smoothScrollTo方法");
Log.d(TAG,"destX向左移動距離;destY向上移動的距離。如果要反向,設置爲負值即可。");
mScroller.startScroll(getScrollX(),getScrollY(),destX,destY,1000);
invalidate();
Log.d(TAG,"注意:移動的是自定義View裏面的內容(所有子view一起移動),背景不會動。例如我們自定義View是繼承LinearLayout,裏面所有的子view都是移動");
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
Log.d(TAG,"步驟3:重寫View的computeScroll方法");
Log.d(TAG,"會頻繁地調用這個方法,通過scrollTo方法到達緩慢滾動的目的。");
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
}
xml配置(很重要,需要有子元素,否則看不出來滑動了)
<com.example.testgesturedetector.MyViewScroller
android:id="@+id/myView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:orientation="vertical">
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:background="#00ff00"
android:gravity="center"
android:text="點擊進行scroller滑動"
android:textColor="#000000" />
</com.example.testgesturedetector.MyViewScroller>
運行的效果如下:
2.通過動畫
動畫本身就是一種漸進的過程,因此通過他來實現滑動天然就具有彈性效果,比如以下代碼讓一個view在100ms內左移100像素
ObjectAnimator.ofFloat(testView, "translationX", 0, 100).setDuration(100).start();
不過這裏想說的並不是這個問題,我們可用利用動畫的特性來實現一些動畫不能實現的效果,還拿scorllTo來說,我們想模仿scroller來實現View的彈性滑動,那麼利用動畫的特性我們可用這樣做:
final int startX = 0;
final int startY = 100;
final int deltaX = 0;
final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float fraction = animator.getAnimatedFraction();
testView.scrollTo(startX + (int)(deltaX * fraction),0);
}
});
3.使用延時策略
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int count = 1;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MESSAGE_SCROLL_TO:
count++;
if(count <= FRAME_COUNT){
float fraction = count / (float)FRAME_COUNT;
int scrollX = (int)(fraction * 100);
testButton.scrollTo(scrollX,0);
handler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
}
break;
}
}
};
上面集中彈性滑動的實現方法,在介紹中側重更多的是實現思想,在實際使用中過程中可以對其進行靈活的擴展從而實現更加複雜的效果。
到此爲止,View的滑動了解的差不了。下面來聊聊View的事件分發傳遞。