1. 程式人生 > >Android--自定義View滑動的六種方法

Android--自定義View滑動的六種方法

概述

由於移動平臺螢幕尺寸大小的限制,為了給使用者呈現更好的頁面內容,我們需要通過滑動來實現一些頁面內容的顯示和隱藏操作。

自定義View的方法

  • View.layout()方法
  • 改變view的佈局引數LayoutParams
  • View.offsetLeftAndRight()和View.offsetTopAndBottom()方法
  • 屬性動畫ObjectAnimator.ofFloat()方法
  • 使用View動畫
  • View.scrollto()和View.scrollby()方法
  • 各種方法的比較

例子

實現自定義view跟隨螢幕的點選和拖動自動滑動:程式碼如下:
自定義view:滑動方法在redirectViewPosition中

package com.example.myscroller;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;


/**
 * Created by pingkun.huang on 2016/4/21.
 */
public class MyView1 extends View{

    public MyView1(Context context) {
        super(context);

    }

    public
MyView1(Context context, AttributeSet attrs) { super(context, attrs); } public MyView1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void redirectViewPosition(float clickXPosition, float clickYPosition) { int
offsetX = (int) (clickXPosition - getLeft()); int offsetY = (int) (clickYPosition - getTop()); //-------第一種方法 呼叫View的layout()方法 layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); //-------第二種方法 改變View的佈局引數 ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams(); marginLayoutParams.leftMargin = getLeft() + offsetX; marginLayoutParams.topMargin = getTop() + offsetY; //可以用requestLayout()或者setLayoutParams setLayoutParams(marginLayoutParams); //-------第三種方法 offsetLeftAndRight(offsetX); offsetTopAndBottom(offsetY); } }

Activity:

package com.example.myscroller;

import android.animation.ObjectAnimator;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Scroller;

public class MainActivity extends AppCompatActivity {
    MyView1 myView1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myView1 = (MyView1) this.findViewById(R.id.hpk);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                myView1.redirectViewPosition(event.getX(), event.getY());   
            case MotionEvent.ACTION_MOVE:
                myView1.redirectViewPosition(event.getX(),event.getY());

                break;
        }
        return super.onTouchEvent(event);
    }
}

佈局檔案:

<?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.example.myscroller.MainActivity">
    <com.example.myscroller.MyView1
        android:id="@+id/hpk"
        android:background="#00ff00"
        android:layout_width="100dp"
        android:layout_height="100dp" >
        </com.example.myscroller.MyView1>
</RelativeLayout>

1:View.layout()

view在被繪製到螢幕上的時候會先呼叫onMeasure()方法來確定view的大小,之後會呼叫onLayout()方法來告訴父容器view的具體位置,view位置的確定有四個引數來決定left、top、right、bottom,我們可以通過改變這四個引數來實現View的滑動。layout()方法的宣告為:

public void layout(int left, int top, int right, int bottom)

left:view的左邊距和父容器的左邊距的差
top:view的上邊距和父容器的上邊距的差
right:view的右邊距和父容器的左邊距的差
bottom:veiw的下邊距和父容器的上邊距的差

view的這四個引數都是相對它的父容器而言的。原始值可通過getLeft(),getTop(),getRight(),getBottom()來獲取,且通過這四個函式獲取的值是固定的,拖放過程中並不會改變著四個函式的返回值。

public void redirectViewPosition(float clickXPosition, float clickYPosition) {
        int offsetX = (int) (clickXPosition - getLeft());
        int offsetY = (int) (clickYPosition - getTop());
        //第一種方法 呼叫View的layout()方法
        layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

clickXposition:當前螢幕點選的x座標可通過MotionEvent.getX()來取得。
clickYposition:當前螢幕點選的y座標可通過MotionEvetnt.getY()來取得
offsetX:當前點選位置的x座標和getLeft()返回值的差值,就是滑動後x軸偏移量
offsetY:當前點選位置的y座標和getTop()返回值的差值,就是滑動後y軸的偏移量。
最後計算view當前位置相對於父容器的left,top,right,bottom值。在螢幕滑動過程中會不斷的呼叫layout方法,重新繪製view。
注意:
getX/getY返回的是相對於當前View父容器左上角的x座標和y座標,而getRawX/getRawY 返回的是相對於手機螢幕左上角的x和y座標

2:改變view的佈局引數LayoutParams

LayoutParams儲存了View的位置引數,因此我們可以通過改變LayoutParams的屬性值來打到滑動View的目的。

ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
        marginLayoutParams.leftMargin = getLeft() + offsetX;
        marginLayoutParams.topMargin = getTop() + offsetY;
        //可以用requestLayout()或者setLayoutParams
        setLayoutParams(marginLayoutParams);

MarginLayoutParams類 繼承自ViewGroup.LayoutParams,有leftMargin,topMargin,rightMargin,bottomMargin等變數。因此我們可以修改這些margin值,之後呼叫setLayoutParams或requestLayout來使這些修改後的變數生效。

3:View.offsetLeftAndRight()和View.offsetTopAndBottom()方法

offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);

這兩個方法分別設定了view的水平和垂直偏移量,其實在函式內部利用偏移量計算了view當前相對於父佈局的上,下,左,右邊距。內部中mLeft += offset; mRight += offset; mTop += offset; mBottom += offset;

public void offsetLeftAndRight(int offset) {
        if (offset != 0) {
            final boolean matrixIsIdentity = hasIdentityMatrix();
            if (matrixIsIdentity) {
                if (isHardwareAccelerated()) {
                    invalidateViewProperty(false, false);
                } else {
                    final ViewParent p = mParent;
                    if (p != null && mAttachInfo != null) {
                        final Rect r = mAttachInfo.mTmpInvalRect;
                        int minLeft;
                        int maxRight;
                        if (offset < 0) {
                            minLeft = mLeft + offset;
                            maxRight = mRight;
                        } else {
                            minLeft = mLeft;
                            maxRight = mRight + offset;
                        }
                        r.set(0, 0, maxRight - minLeft, mBottom - mTop);
                        p.invalidateChild(this, r);
                    }
                }
            } else {
                invalidateViewProperty(false, false);
            }

            mLeft += offset;
            mRight += offset;

            mRenderNode.offsetLeftAndRight(offset);
            if (isHardwareAccelerated()) {
                invalidateViewProperty(false, false);
                invalidateParentIfNeededAndWasQuickRejected();
            } else {
                if (!matrixIsIdentity) {
                    invalidateViewProperty(false, true);
                }
                invalidateParentIfNeeded();
            }
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
    }

屬性動畫ObjectAnimator.ofFloat()方法

ObjectAnimator.ofFloat(myView1,"translationX",0,100).setDuration(100).start();

函式簽名為:

public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
        ObjectAnimator anim = new ObjectAnimator(target, propertyName);
        anim.setFloatValues(values);
        return anim;
    }

target:屬性動畫所作用的物件
propertyName:target物件的動畫屬性,必須在target內部有相應的set方法。你要設定動畫的物件的屬性必須有一個set該值的方法。因為ObjectAnimator在動畫的過程中自動更新屬性值,這是通過呼叫該屬性的set方法來實現的
values:動畫屬性的起始值和結束值。
可以參考:這篇文章

使用View動畫

使用動畫主要是改變View的translationX和translationY屬性

Animation animation = AnimationUtils.loadAnimation(this, R.anim.weilin);
 myView1.startAnimation(animation);

loadAnimation來載入xml格式的動畫檔案,之後呼叫startAnimation來啟動動畫。

res目錄下新建一個anim資料夾,建立一個動畫資源,如下:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:duration="100"
    android:shareInterpolator="@android:anim/accelerate_interpolator">
    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="100"
        android:toYDelta="100" />
</set>

注意:
1:View動畫是對View的影像操作,並不能真正改變view的位置引數和寬高,如果希望動畫完之後的狀態可以保留必須將fillAfter屬性設為true,否則動畫在完成後的一瞬間就會恢復原樣。
2:平移後的view不能響應view上的事件,因為我們操作的是view的副本,在系統看來實際的view還是在原來的位置。

View.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();
            }
        }
    }
public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

注意:
1:scrollTo和scrollBy只能改變View內容的位置,不能改變View在佈局中的位置
2:scrollTo是基於所傳遞引數的絕對滑動,scrollBy是相對於當前位置的相對滑動。
3:從函式中可以看出scrollBy實際上是呼叫了scrollTo方法。
4:mScrollX = View左邊緣和View內容的左邊緣的水平距離
mScrollY = Viewe上邊緣和View內容的上邊緣在豎直方向上的距離
view的邊緣是指view的位置,由四個頂點組成。
view內容的邊緣時指view中內容的邊緣
5:如果view內容向view邊界的左上角移動,則mScrollX和mScrollY為正,向左下角移動為負。

實現View中的內容跟隨手指移動

package com.example.myscroller;

import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

/**
 * Created by pingkun.huang on 2016/4/20.
 */
public class MyView extends TextView implements View.OnTouchListener,GestureDetector.OnGestureListener,GestureDetector.OnDoubleTapListener{
    GestureDetector gestureDetector;
    Context context;
    public MyView(Context context) {
        super(context);
        initData(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initData(context);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initData(context);
    }

    private void initData(Context context) {
        this.setText("測試文字");
        this.setTextSize(30);
        this.context = context;
        this.setOnTouchListener(this);
        super.setClickable(true);
        super.setLongClickable(true);
        super.setFocusable(true);
        gestureDetector = new GestureDetector(context, this);
        gestureDetector.setOnDoubleTapListener(this);
    }


    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
    }


    @Override
    public boolean onDown(MotionEvent e) {
        int x = (int) e.getX();
        int y = (int) e.getY();
        this.scrollTo(-x,-y);
        return true;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return true;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        int x = (int) e2.getX();
        int y = (int) e2.getY();
        this.scrollTo(-x,-y);
        //this.scrollBy((int)distanceX,(int)distanceY);
        return true;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return true;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        return true;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        this.setTextSize(this.getTextSize()+2);
        return true;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        return true;
    }
}

各種方法的比較

scrollTo和scrollBy適合View的內容滑動。
動畫滑動:適合沒有互動的View和複雜的動畫效果。
佈局引數:適用於有互動的View。