Android拖拽動畫實現

前言

       在Android開發過程中,經常會遇到需要實現拖拽動畫,拖拽動畫的實現比較簡單,可以採用多種方式來進行實現,這裏主要是因爲在使用過程中遇到了一種不常見的情況,因此記錄一下。

拖拽實現

       這裏我們先寫一個demo來實現拖拽動畫, 效果如下:

這裏寫圖片描述

       上面的效果就是最終需要實現的效果,按住可以拖動,放開手指後,向靠近的一邊移動比貼邊,如果是點擊則處理點擊事件,其實就是微信視頻通話時小攝像頭動畫的效果。這裏我們就來實現一下該效果。

基本View實現

       首先我們來自定義一個View,該View可以拖動,從上面的效果圖中我們可以看到,View與手機邊框是有一個間隔的,該間隔我們來採用定義屬性實現。因此我們先定義一個自定義View和attr,代碼如下:

       首先我們來定義attr,在attr.xml中加入如下自頂一個屬性:

<declare-styleable name="DragFrameLayout">
    <attr name="margin_edge" format="dimension"></attr>
</declare-styleable>

       DragFrameLayout就是我們自定義的View,他繼承自FrameLayout,因爲他最終會加入其它的View,來進行一起拖拽,接着我們來定義View:


public class DragFrameLayout extends FrameLayout {

    public int margin_edge;

    private int width, height;

    private int viewHeight;

    private int viewWidth;

    private int statusBarHeight;

    public DragFrameLayout(@NonNull Context context) {
        super(context);
        init(context, null);
    }

    public DragFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public DragFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        resolveAttr(context, attrs);
        DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();
        width = displayMetrics.widthPixels;
        height = displayMetrics.heightPixels;//
        statusBarHeight = getStatusBarHeight();
        if (statusBarHeight == 0) {
            statusBarHeight = (int) (25 * displayMetrics.scaledDensity + 0.5f);
        }
        height -= statusBarHeight;
        //還需要減去actionBar的高度
        margin_edge = 10;
    }


    private void resolveAttr(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DragFrameLayout);
        margin_edge = array.getDimensionPixelSize(R.styleable.DragFrameLayout_margin_edge, 10);
        array.recycle();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        viewWidth = getWidth();
        viewHeight = getHeight();
    }


    public int getStatusBarHeight() {
        if (statusBarHeight == 0) {
            try {
                Class<?> c = Class.forName("com.android.internal.R$dimen");
                Object o = c.newInstance();
                Field field = c.getField("status_bar_height");
                int x = (Integer) field.get(o);
                statusBarHeight = getResources().getDimensionPixelSize(x);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return statusBarHeight;
    }

}

       這裏一個基本的View就實現了,與邊界的間距我們採用了自定義attr來實現,如果還有其他的屬性,我們都可以採用自定義attr來實現。

       這裏我們首先解析了attr,之後獲取的狀態欄的高度,因爲我們最終是在可見區域整個屏幕移動的,獲取的高包括了狀態欄和標題欄,所以當拖動到底部的時候需要修正高度,這裏主要做演示就不在獲取標題欄高度了。之後我們在onSizeChanged獲取了view的寬高。

處理onTouchEvent

@Override
public boolean onTouchEvent(MotionEvent event) {
    curX = event.getRawX();
    curY = event.getRawY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downX = lastX = event.getRawX();
            downY = lastY = event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            onMove();
            lastX = curX;
            lastY = curY;
            break;
        case MotionEvent.ACTION_UP:
            onScrollEdge();
            break;
    }
    return true;
}

       這裏我們對所有事件都返回了true,表示我們關係該事件和後續的事件。
       我們記錄點擊的位置和每次move移動的位置,我們後續需要根據這些位置數據進行移動處理。

實現onMove

       上面我們已經獲取了移動前後的位置,根據移動前後的位置來進行移動。代碼如下:

private void onMove() {
    int dx = (int) (curX - lastX);
    int dy = (int) (curY - lastY);

    if (getLeft() + dx < margin_edge) {
        dx = 0;
    } else if (getLeft() + viewWidth + dx > width - margin_edge) {
        dx = 0;
    }

    if (getTop() + dy < margin_edge) {
        dy = 0;
    } else if (getTop() + viewHeight + dy > height - margin_edge) {
        dy = 0;
    }
    LogUtil.e("onMove: getLeft=" + getLeft() + " dx=" + dx + " dy=" + dy);
}

       這裏我們需要先計算移動的距離,之後需要對移動的距離進行修正,不能移動到屏幕外面,需要距離屏幕邊框一定的距離。修正了移動的距離後,我們就是用修正的位置進行移動。

Layout方式

        我們知道layout可以改變View的位置,因此我們這裏採用layout方式進行移動:

private void setPosition1(int dx, int dy) {
    layout(getLeft() + dx, getTop() + dy, getLeft() + viewWidth + dx, getTop() + viewHeight + dy);
}

       我們獲取上一次的位置加上偏移量進行Layout設置。

offset方式實現

       可以調用兩個函數offsetLeftAndRight,offsetTopAndBottom這是系統提供的,設置View偏移的距離。

private void setPosition2(int dx, int dy) {
    offsetLeftAndRight(dx);
    offsetTopAndBottom(dy);
}

setTranslation*實現

       這裏我們就不能僅僅採用dx,dy來進行移動了, translation函數不是偏移量,而是每次移動的距離,比如1到100,移動100次,每次偏移1,其他方式都是設置1進行偏移,而該函數是需要累加的。移動50次需要設置50。我們的屬性動畫很多時候就是使用的該屬性。

int transX = 0;
int transY = 0;

/**
 * 需要修正實現方式
 * @param dx
 * @param dy
 */
private void setPosition3(int dx, int dy) {
    transX += dx;
    transY += dy;
    setTranslationX(transX);
    setTranslationY(transY);
}

layoutParams實現

        我們知道可以對view設置佈局參數也能設置view的位置,這裏我們就來設置View的佈局參數:

private void setPosition4(int dx, int dy) {
    LayoutParams layoutParams = (LayoutParams) getLayoutParams();
    layoutParams.rightMargin = layoutParams.rightMargin - dx;
    layoutParams.topMargin = layoutParams.topMargin + dy;
    setLayoutParams(layoutParams);
}

       上面我們爲什麼要採用rightMargin而不是LeftMargin吶?這是由於我們初始的時候對view設置的Gravity參數,參數爲top和right,因此leftMargin初始是0,這裏是需要注意的地方, 後續的使用與Gravity參數相關。

貼邊實現

       上面我們實現了移動,還有一部分效果就是擡手後,需要滾動到靠近的一邊,這裏我們採用Scroller來實現。在init中初始化Scroller,後續使用該對象,同時我們需要處理點擊事件,點擊事件我們需要回調給調用者,所以我們先定義一個回調,同時設置回調:

public void setCallback(Callback callback) {
    this.callback = callback;
}

public interface Callback {
    void onClick();
}

       我們來處理擡起事件,這裏也需要計算那邊靠的更近:

private void onScrollEdge() {
    LogUtil.e("scroll: getScrollX=" + getScrollX() + " getScrollY=" + getScrollY());
    if (Math.abs(curX - downX) < TOUCH_THRESHOLD && Math.abs(curY - downY) < TOUCH_THRESHOLD) {
        if (callback != null) {
            callback.onClick();
        }
        return;
    }
    int dx;
    if (getLeft() > (width - getRight())) {
        dx = width - getRight() - margin_edge;
    } else {
        dx = margin_edge - getLeft();
    }
    lastOffset = 0;
    scroller.startScroll(getScrollX(), getScrollY(), dx, 0);
    invalidate();
}

       首先判斷是否是點擊事件,判斷初始位置與擡起位置是否小於某一個閾值,小於則認爲是點擊事件,不過這個地方還可以預防一下就是拖動後在拖回到原位置,因爲是靠近的一邊,因此y方向是不變的。調用了startScroll後,我們需要複寫computeScroll, 在ondraw會用中會調用該函數:


@Override
public void computeScroll() {
    if (scroller.computeScrollOffset()) {
        LogUtil.e("scroll: getLeft=" + getLeft() + " currX=" + scroller.getCurrX());
        int dx = scroller.getCurrX() - lastOffset;
        lastOffset = scroller.getCurrX();
        setPosition1(dx, 0);
        //setPosition2(dx, 0);
        //setPosition3(dx, 0);
        //setPosition4(dx, 0);
        invalidate();
    }
    super.computeScroll();
}

       這裏我們需要是偏移量,而scroller.getCurrX()獲取的是累積值,因此我們要先記住上一次的偏移距離得出兩次移動的偏移量。

       不過這裏setPosition3是有問題的,上述計算的方式適用於1,2,4,方法3需要調整,有需要的可以自己來進行調整。

       很多人可能有疑問了,爲什麼不採用scrollBy,scrollTo來進行實現,這裏需要說明一點的是,scrollTo,scrollBy滾動的是控件的內容,而不是控件本身,因此上面的控件需要移動,需要調用getParent().scroll*函數。但是往往父元素不僅僅有一個子元素,其他的原生也會跟着一起移動,解決這種問題就需要在該View外面再套一層,這樣會加深佈局

最終實現

       前面分佈實現了效果,這裏我們來看一下完整的代碼:

package com.demo.demo.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.AttrRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import android.widget.Scroller;

import com.demo.demo.R;
import com.demo.demo.util.LogUtil;

import java.lang.reflect.Field;

public class DragFrameLayout extends FrameLayout {

    public static final int TOUCH_THRESHOLD = 5;

    public int margin_edge;

    private Scroller scroller;

    private float downX, downY;

    private float lastX, lastY;

    private float curX, curY;

    private int lastOffset;

    private int width, height;

    private int viewHeight;

    private int viewWidth;

    private int statusBarHeight;

    private Callback callback;

    public DragFrameLayout(@NonNull Context context) {
        super(context);
        init(context, null);
    }

    public DragFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public DragFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        resolveAttr(context, attrs);
        scroller = new Scroller(getContext());
        DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();
        width = displayMetrics.widthPixels;
        height = displayMetrics.heightPixels;//
        statusBarHeight = getStatusBarHeight();
        if (statusBarHeight == 0) {
            statusBarHeight = (int) (25 * displayMetrics.scaledDensity + 0.5f);
        }
        height -= statusBarHeight;
        //還需要減去actionBar的高度
        margin_edge = 10;
    }


    private void resolveAttr(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DragFrameLayout);
        margin_edge = array.getDimensionPixelSize(R.styleable.DragFrameLayout_margin_edge, 10);
        array.recycle();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        viewWidth = getWidth();
        viewHeight = getHeight();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (scroller.computeScrollOffset()) {
            return super.onTouchEvent(event);
        }
        curX = event.getRawX();
        curY = event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = lastX = event.getRawX();
                downY = lastY = event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                onMove();
                lastX = curX;
                lastY = curY;
                break;
            case MotionEvent.ACTION_UP:
                onScrollEdge();
                break;
        }
        return true;
    }

    private void onMove() {
        int dx = (int) (curX - lastX);
        int dy = (int) (curY - lastY);

        if (getLeft() + dx < margin_edge) {
            dx = 0;
        } else if (getLeft() + viewWidth + dx > width - margin_edge) {
            dx = 0;
        }

        if (getTop() + dy < margin_edge) {
            dy = 0;
        } else if (getTop() + viewHeight + dy > height - margin_edge) {
            dy = 0;
        }
        LogUtil.e("onMove: getLeft=" + getLeft() + " dx=" + dx + " dy=" + dy);
        setPosition1(dx, dy);
        //setPosition2(dx, dy);
        //setPosition3(dx, dy);
        //setPosition4(dx, dy);
    }

    private void setPosition1(int dx, int dy) {
        layout(getLeft() + dx, getTop() + dy, getLeft() + viewWidth + dx, getTop() + viewHeight + dy);
    }

    private void setPosition2(int dx, int dy) {
        offsetLeftAndRight(dx);
        offsetTopAndBottom(dy);
    }

    int transX = 0;
    int transY = 0;

    /**
     * 需要修正實現方式
     * @param dx
     * @param dy
     */
    private void setPosition3(int dx, int dy) {
        transX += dx;
        transY += dy;
        setTranslationX(transX);
        setTranslationY(transY);
    }

    private void setPosition4(int dx, int dy) {
        LayoutParams layoutParams = (LayoutParams) getLayoutParams();
        layoutParams.rightMargin = layoutParams.rightMargin - dx;
        layoutParams.topMargin = layoutParams.topMargin + dy;
        setLayoutParams(layoutParams);
    }

    private void onScrollEdge() {
        LogUtil.e("scroll: getScrollX=" + getScrollX() + " getScrollY=" + getScrollY());
        if (Math.abs(curX - downX) < TOUCH_THRESHOLD && Math.abs(curY - downY) < TOUCH_THRESHOLD) {
            if (callback != null) {
                callback.onClick();
            }
            return;
        }
        int dx;
        if (getLeft() > (width - getRight())) {
            dx = width - getRight() - margin_edge;
        } else {
            dx = margin_edge - getLeft();
        }
        lastOffset = 0;
        scroller.startScroll(getScrollX(), getScrollY(), dx, 0);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            LogUtil.e("scroll: getLeft=" + getLeft() + " currX=" + scroller.getCurrX());
            int dx = scroller.getCurrX() - lastOffset;
            lastOffset = scroller.getCurrX();
            setPosition1(dx, 0);
            //setPosition2(dx, 0);
            //setPosition3(dx, 0);
            //setPosition4(dx, 0);
            invalidate();
        }
        super.computeScroll();
    }

    public int getStatusBarHeight() {
        if (statusBarHeight == 0) {
            try {
                Class<?> c = Class.forName("com.android.internal.R$dimen");
                Object o = c.newInstance();
                Field field = c.getField("status_bar_height");
                int x = (Integer) field.get(o);
                statusBarHeight = getResources().getDimensionPixelSize(x);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return statusBarHeight;
    }

    public void setCallback(Callback callback) {
        this.callback = callback;
    }

    public interface Callback {
        void onClick();
    }
}

有問題?

       上面說了這麼多都還沒有到今天的主題,在Dome中效果還是還不錯吧!移動流暢,貼邊也效果不錯。那到底有什麼問題?

       問題就是當該控件與SurfaceView一起使用時,會出現問題,當有多幀View,後面是SurfaceView進程視頻預覽,前面是拖拽View,這個時候拖拽View會自動回到初始位置,當採用方法1,方法2的時候,拖動View後,View會自動回到右上角。只有方法4不會,因此方法四確實改變了View的margin距離。這也是爲什麼我採用了多種方式來實現。前面的方法都是重繪後面的方法重新佈局。

Code

       代碼Git地址如下:

       -Code1
        Code2

總結

       拖拽動畫很簡單,但是在使用時還是會遇到坑。不在特定的情況下,是不能復現該問題。也需要去探究控件與SurfaceView結合時界面到底是怎麼繪製的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章