废话不多说,开门见山,实现的效果如下图所示,手指能拖拽悬浮球移动,并会根据距离判断自动吸附于屏幕边缘,其半边隐藏于屏幕外。
方式一:自定义View重写onTouchEvent
onTouchEvent(MotionEvent event)是手机屏幕事件的处理方法,参数event为手机屏幕触摸事件封装类的对象,其中封装了该事件的所有信息;例如触摸的位置、触摸的类型以及触摸的时间等;该对象会在用户触摸手机屏幕时被创建。
开发中经常要用到的状态有以下三种,可以通过event.getAction()获得。
- 手指按下:MotionEvent.ACTION_DOWN
- 手指在屏幕上移动:MotionEvent.ACTION_MOVE
- 手指抬起:MotionEvent.ACTION_UP
现在通过一个示例来实现可拖拽、自动吸附的控件。自定义一个MovedImageButton继承自ImageButton,然后重写onTouchEvent方法,在该方法中,event.getX()、event.getY() 是用来获取触摸点在控件上的实时位置,然后用 lastX、lastY 来记录按下的瞬间触摸点在控件上的位置。
ACTION_MOVE 状态触发后,计算出手指在x轴和y轴的偏移量,再调用 layout(int l,int t,int r,int b) 方法,此方法用来重新绘制控件位置,4个参数分别表示控件左上角(l、t)和右下角(r,b)相对于 屏幕 的坐标,当然也可以用offsetLeftAndRight(int offsetX)、offsetTopAndBottom(int offsetY) 方法实现同样效果;最后手指离开屏幕的瞬间。
触发ACTION_UP状态后,此时控件需要移动到屏幕边缘,实现吸附效果,这里使用了ObjectAnimator或是控件自带的animate() 方法来实现自动移动的动画,另外添加了对距离的判断,以此选择停靠在屏幕哪个边缘。
public class MovedImageButton extends android.support.v7.widget.AppCompatImageButton {
private int lastX;
private int lastY;
private float screenWidth;
public MovedImageButton(Context context) {
super(context);
}
public MovedImageButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MovedImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
DisplayMetrics dm = getResources().getDisplayMetrics();
screenWidth = dm.widthPixels;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x-lastX;
int offsetY = y-lastY;
//第一种方法
layout(getLeft()+offsetX,
getTop()+offsetY,
getRight()+offsetX,
getBottom()+offsetY);
//第二种方法
//offsetLeftAndRight(offsetX);
//offsetTopAndBottom(offsetY);
break;
case MotionEvent.ACTION_UP:
adsorbAnim(event.getRawX(), event.getRawY());
break;
}
return super.onTouchEvent(event);
}
private void adsorbAnim(float rawX, float rawY){
//靠顶吸附
if (rawY <= MeasureUtil.dp2px(getContext(),200)){//注意rawY包含了标题栏的高
animate().setDuration(400)
.setInterpolator(new OvershootInterpolator())
.yBy(-getY()-getHeight()/2.0f)
.start();
}else if (rawX >= screenWidth/2){//靠右吸附
animate().setDuration(400)
.setInterpolator(new OvershootInterpolator())
.xBy(screenWidth - getX() - getWidth()/2.0f)
.start();
}else {//靠左吸附
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "x", getX(), -getWidth()/2.0f);
animator.setInterpolator(new OvershootInterpolator());
animator.setDuration(400);
animator.start();
}
}
}
接着在活动的xml文件中添加该控件,为了达到更加自然的交互效果,这里使用了android:stateListAnimator,它可以监听一些状态(例如:按压—松开、获取到焦点—失去焦点、被选中—未被选中等等),来实现动画效果,使用起来很方便。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.bluefire.floatviewtest.MovedImageButton
android:id="@+id/pointMoved"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="8dp"
android:background="#00FFFFFF"
android:src="@drawable/point_ic"
android:stateListAnimator="@animator/pressed_state_list"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:targetApi="lollipop" />
</android.support.constraint.ConstraintLayout>
接下来我们就来创建一个状态动画,在res文件夹下新建一个animator资源文件夹,在animator文件夹下命名pressed_state_list的xml文件,然后在这里定义我们需要的StateListAnimator。下面设置的效果就是手指按压控件后,控件按照其中心扩大1.3倍,并且透明度为原来的0.6,Z方向位置为2;手指松开后,控件大小和透明度恢复原状。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<set>
<objectAnimator android:duration="@android:integer/config_shortAnimTime"
android:propertyName="scaleX" android:valueTo="1.3"
android:valueType="floatType" />
<objectAnimator android:duration="@android:integer/config_shortAnimTime"
android:propertyName="scaleY" android:valueTo="1.3"
android:valueType="floatType" />
<objectAnimator android:duration="@android:integer/config_shortAnimTime"
android:propertyName="alpha" android:valueTo="0.6"
android:valueType="floatType" />
<objectAnimator android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ" android:valueTo="2"
android:valueType="intType"/>
</set>
</item>
<item android:state_pressed="false">
<set>
<objectAnimator android:duration="@android:integer/config_shortAnimTime"
android:propertyName="scaleX" android:valueTo="1.0"
android:valueType="floatType" />
<objectAnimator android:duration="@android:integer/config_shortAnimTime"
android:propertyName="scaleY" android:valueTo="1.0"
android:valueType="floatType" />
<objectAnimator android:duration="@android:integer/config_shortAnimTime"
android:propertyName="alpha" android:valueTo="1.0"
android:valueType="floatType" />
<objectAnimator android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ" android:valueTo="1"
android:valueType="intType"/>
</set>
</item>
</selector>
以上这些就是用第一种方法实现控件拖拽和自动吸附屏幕边缘效果的全部内容,是不是很简单,接下来介绍另一方法。
方式二:使用WindowManager
WindowManager和Window这两个概念经常被放在一起讲,有关它们的详细介绍可以参考这篇文章:https://blog.csdn.net/joejames/article/details/79700503
实现拖拽功能,首先需要用到的是WindowManager的addView (View view, ViewGroup.LayoutParams params) 方法,需要注意的是params一定要设置flags、type的值。
- Flags参数表示Window的属性,它有很多选项,通过这些选项可以控制Window的显示特性,这里主要介绍几个比较常用的选项。
选项 | 说明 |
---|---|
FLAG_NOT_FOCUSABLE | 表示Window不需要获取焦点,也不需要接收各种输入事件,此标记会同时启用FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的Window。 |
FLAG_NOT_TOUCH_MODAL | 系统会将当前Window区域以外的单击事件传递给底层的Window,当前Window区域以内的单击事件则自己处理。 |
FLAG_SHOW_WHEN_LOCKED | 此模式可以让Window显示在锁屏的界面上。 |
- Type参数表示Window的类型,Window有三种类型,分别是Window、子Window和系统Window。应用Window对应着一个Activity。子Window不能单独存在,他需要附属在特定的父Window中,比如Dialog。系统Window是需要申明权限才能创建的Window,比如系统状态栏和Toast。以下是部分Type类型。
类型 | 说明 |
---|---|
TYPE_BASE_APPLICATION | 所有程序窗口的“基地”窗口,其他应用程序窗口都显示在它上面。 |
TYPE_APPLICATION | 普通应用功能程序窗口。 |
TYPE_APPLICATION_STARTING | 用于应用程序启动时所显示的窗口,应用本身不要使用这种类型。它用于让系统显示些信息,直到应用程序可以开启自己的窗口。 |
TYPE_KEYGUARD | 锁屏窗口。 |
TYPE_SYSTEM_ERROR | 系统内部错误提示,显示于所有内容之上。 |
接下来需要给控件添加触摸监听及setOnTouchListener,在ACTION_MOVE状态触发的时候,使用WindowManager的updateViewLayout*(View view, ViewGroup.LayoutParams params) 方法更新控件位置,代码如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createRemovedBtn();
}
@SuppressLint("ClickableViewAccessibility")
private void createRemovedBtn() {
final Button button = new Button(this);
button.setText("移动按钮");
final WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT
, WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSLUCENT);
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
layoutParams.gravity = Gravity.START | Gravity.TOP;
layoutParams.x = 200;
layoutParams.y = 200;
windowManager.addView(button, layoutParams);
button.setOnTouchListener(new View.OnTouchListener() {
int lastX;
int lastY;
@Override
public boolean onTouch(View v, MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
layoutParams.x += rawX - lastX;
layoutParams.y += rawY - lastY;
windowManager.updateViewLayout(v, layoutParams);
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
});
}
}
这里的吸附效果就不实现了,其实和方式一介绍的一样,需要注意的是单独使用setOnTouchListener情况下,onTouch方法的返回值为false的时候只会执行down方法,不会执行move和up,只有在为true的时候,三个都会执行。
当setOnTouchListener和setOnclickListener一起使用时,onTouch返回值为true,则不会执行onClick方法,为false的才会执行onClick方法。无论是true还是false,ACTION_DOWN、ACTION_MOVE、ACTION_UP这三个方法都会执行。