Android拖拽动画实现
Posted freeCodeSunny
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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地址如下:
总结
拖拽动画很简单,但是在使用时还是会遇到坑。不在特定的情况下,是不能复现该问题。也需要去探究控件与SurfaceView结合时界面到底是怎么绘制的。
以上是关于Android拖拽动画实现的主要内容,如果未能解决你的问题,请参考以下文章
android开发游记:SpringView 下拉刷新的高效解决方案,定制你自己风格的拖拽页面
android Title滑块动画实现(适合新闻client多种栏目的展示)