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结合时界面到底是怎么绘制的。

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