1. layout方法
根据用户手指滑动的位置(ACTION_MOVE),记录每次小段的偏移量(offset),通过layout不停对view进行重新布局,完成view的移动效果。
a). 使用视图坐标系:getX(),getY()
@Override
public boolean onTouchEvent(MotionEvent event) {
// 这里需要转换为int值,保证layout方法参数
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
break;
}
// 返回false不能达到滑动的目标
return true;
}
}
由于每次layout后,触摸点相对于父控件的位置不变,因此滑动期间不需要更新mLastX和mLastY的值。
b). 使用Android坐标系:getRawX(),getRawY()
@Override
public boolean onTouchEvent(MotionEvent event) {
// 这里需要转换为int值,保证layout方法参数
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
// 需要更新mLastX与mLastY的值
mLastX = x;
mLastY = y;
break;
}
// 返回false不能达到滑动的目标
return true;
}
需要注意的是,每次move更新界面后都要更新上一次的坐标值,否则下一次布局时会加上控件到屏幕边缘的距离。
2. offsetLeftAndRight()、offsetTopAndBottom()方法:和第一种方法没啥区别
@Override
public boolean onTouchEvent(MotionEvent event) {
// 这里需要转换为int值,保证layout方法参数
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
}
// 返回false不能达到滑动的目标
return true;
}
3. layoutParams: 利用设置边距完成view的移动
@Override
public boolean onTouchEvent(MotionEvent event) {
// 这里需要转换为int值,保证layout方法参数
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
ViewGroup.MarginLayoutParams layoutParams =
(ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
}
// 返回false不能达到滑动的目标
return true;
}
注意:使用ViewGroup必须保证该控件有一个父布局,否则不能使用
4. scrollBy、scrollTo:瞬间移动
@Override
public boolean onTouchEvent(MotionEvent event) {
// 这里需要转换为int值,保证layout方法参数
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
// scrollBy和scrollTo的移动方向为屏幕移动方向,与控件移动方向相反
// scrollBy移动的是content,不是view
((View) getParent()).scrollBy(-offsetX, -offsetY);
break;
}
// 返回false不能达到滑动的目标
return true;
}
注意:scroll方法移动的不是控件本身,而是其内容。举例来说,View为ViewGroup时,移动的是其全部子控件;View为TextView时,移动的是其文字内容。
5. Scroller:实现平滑移动,在移动模块的同时增加松手后回弹至初始位置的功能。
public class Rect extends View {
private int mLastX;
private int mLastY;
private Scroller mScroller;
public Rect(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
public void computeScroll() {
super.computeScroll();
// 如果Scroller还在计算中,则另其移动
if (mScroller.computeScrollOffset()) {
((View)getParent()).scrollTo(mScroller.getCurrX(),
mScroller.getCurrY());
invalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 这里需要转换为int值,保证layout方法参数
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
// scrollBy和scrollTo的移动方向为屏幕移动方向,与控件移动方向相反
// scrollBy移动的是content,不是view
((View) getParent()).scrollBy(-offsetX, -offsetY);
break;
case MotionEvent.ACTION_UP:
View viewGroup = (View) getParent();
// 这里的起始坐标为content的起始坐标,而不是view的起始坐标
mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(),
-viewGroup.getScrollX(), -viewGroup.getScrollY());
invalidate();
}
// 返回false不能达到滑动的目标
return true;
}
}
步骤分为:初始化Scroller对象——复写computeScroll函数——调用Scroller对象的startScroll函数开启滑动过程
6. 属性动画
后期待添加
7. ViewDragHelper
xml文件:
<?xml version="1.0" encoding="utf-8"?>
<com.example.tianshuhe.learningcomponent.DragFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.tianshuhe.learningcomponent.MainActivity">
<View
android:id="@+id/menu_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"/>
<View
android:id="@+id/main_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_red_light"/>
</com.example.tianshuhe.learningcomponent.DragFrameLayout>
自定义的DragFrameLayout文件:
package com.example.tianshuhe.learningcomponent;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
/**
* Created by tianshuhe on 17/8/22.
*/
public class DragFrameLayout extends FrameLayout {
private ViewDragHelper mViewDragHelper;
private ViewDragHelper.Callback mCallback;
private View mMainView, mMenuView;
public DragFrameLayout (Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
mCallback = new ViewDragHelper.Callback() {
@Override
// 只拦截主界面的滑动事件
public boolean tryCaptureView(View child, int pointerId) {
return mMainView == child;
}
// 设置水平滑动事件
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
// 根据滑动的大小,设置是否显示全部的菜单
// 这种实现方式实际上是一开始menu被main阻挡,后期main被拿开
if (mMainView.getLeft() < 500) {
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragFrameLayout.this);
} else {
mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
ViewCompat.postInvalidateOnAnimation(DragFrameLayout.this);
}
}
};
mViewDragHelper = ViewDragHelper.create(this, mCallback);
}
// 拦截点击事件
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 这里一开始main和menu使用自定义的滑动view,则返回true,否则不能实现效果
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMainView = findViewById(R.id.main_view);
mMenuView = findViewById(R.id.menu_view);
}
}
一个奇怪的现象:onInterceptTouchEvent中,如果本身的控件带有滑动效果,这种方式返回后并不会对其事件进行拦截,这种情况待后续分析。