在学习这个之前,你首先要了解android的消息机制,Android的坐标系统,android View绘制流程
- scrollBy 个 scrollTo的区别
scrollTo:相对View的初始位置移动的距离。
scrollBy:相对当前位置移动的距离。
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
//mScrollX 当前的X偏移量,mScrollY 当前的Y偏移量
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
- 在实际过程中,例如我们想使控件水平向右移动200dp,那么使用方式:
scrollTo(-200,0);
此时用户可能会很奇怪为什么是-200,因为这个偏移量是相对屏幕左上角移出屏幕外的距离。移出屏幕外的为正,屏幕内的为负。
成员变量mScrollX, mScrollY,相对屏幕左上角已经移出屏幕之外的距离。
Note:假如你给一个LinearLayout调用scrollTo()方法,并不是LinearLayout滚动,而是LinearLayout里面的内容进行滚动,比如你想对一个按钮进行滚动,直接用Button调用scrollTo()一定达不到你的需求,大家可以试一试,如果真要对某个按钮进行scrollTo()滚动的话,我们可以在Button外面包裹一层Layout,然后对Layout调用scrollTo()方法。
- startScroll && computeScrollOffset 方法
public class ScrollerLayout extends ViewGroup {
...
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
View滚动的实现原理,我们先调用Scroller的startScroll()方法来进行一些滚动的初始化设置,然后迫使View进行绘制,我们调用View的invalidate()或postInvalidate()就可以重新绘制View,绘制View的时候会触发computeScroll()方法,我们重写computeScroll(),在computeScroll()里面先调用Scroller的computeScrollOffset()方法来判断滚动有没有结束,如果滚动没有结束我们就调用scrollTo()方法来进行滚动,该scrollTo()方法虽然会重新绘制View,但是我们还是要手动调用下invalidate()或者postInvalidate()来触发界面重绘,重新绘制View又触发computeScroll(),所以就进入一个循环阶段,这样子就实现了在某个时间段里面滚动某段距离的一个平滑的滚动效果也许有人会问,为什么不直接调用scrollTo()方法来实现滚动,其实直接调用是可以,只不过scrollTo()是瞬间滚动的,给人的用户体验不太好,所以Android提供了Scroller类实现平滑滚动的效果。为了方面大家理解,我画了一个简单的scroll实现滚动的原理图
- Scroller 使用
Scroller使用基本步骤:
- 创建Scroller的实例
- (可选)判断刷新时机并调用startScroll()方法来初始化滚动数据并刷新界面
- (可选)重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
package com.example.qwe;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;
public class ScrollerLayout extends ViewGroup {
//滚动实例
private Scroller mScroller;
//判定为移动的最小像素
private int mTouchSlop;
//手机按下的时候屏幕坐标
private float mXDown;
//按住移动的时候屏幕坐标
private float mXMove;
//上次触发ACTION_MOVE事件时的屏幕坐标
private float mXLastMove;
//界面可滚动的左边界
private int mLeftBorder;
//界面可滚动的右边界
private int mRightBorder;
public ScrollerLayout(Context ctx, AttributeSet attrs){
super(ctx,attrs);
// 第一步,创建Scroller的实例
mScroller = new Scroller(ctx);
ViewConfiguration viewConfiguration = ViewConfiguration.get(ctx);
// 获取TouchSlop值
mTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
}
//view绘制三部曲:OnMeasure,OnLayout,OnDraw
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int nChildCount = getChildCount();
for (int i = 0;i < nChildCount;++i){
View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed){
int nChildCount = getChildCount();
for (int i = 0;i < nChildCount;++i){
View childView = getChildAt(i);
//水平布局这三个控件
childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
if(nChildCount > 0){
mLeftBorder = getChildAt(0).getLeft();
mRightBorder = getChildAt(getChildCount() - 1).getRight();
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mXDown = ev.getRawX();
mXLastMove = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
mXMove = ev.getRawX();
float diff = Math.abs(mXMove - mXDown);
//如果达到最小移动单位,则拦截ACTION_MOVE事件
if(diff > mTouchSlop)
return true;
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
mXMove = event.getRawX();
int scrolledX = (int)(mXLastMove - mXMove);
if(getScrollX() + scrolledX < mLeftBorder){
scrollTo(mLeftBorder,0);
return true;
}
else if(getScrollX() + getWidth() + scrolledX > mRightBorder){
scrollTo(mRightBorder - getWidth(),0);
return true;
}
scrollBy(scrolledX,0);
mXLastMove = mXMove;
break;
case MotionEvent.ACTION_UP:
// 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
//这里为什么没用使用scrollTo是为了实现缓冲效果,而不是一下子跳跃滚动
// 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
//为了配合调用startScroll需要重写computeScroll方法
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
super.computeScroll();
// 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
// 返回false表示滚动完成
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
//跳转到指定页
public void goToPage(int targetIndex){
int dx = targetIndex * getWidth() - getScrollX();
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".ScrollerActivity">
<com.example.qwe.ScrollerLayout
android:id="@+id/scroll_Layout"
android:layout_width="match_parent"
android:layout_height="100dp">
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="This is first child view"/>
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="This is second child view"/>
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="This is third child view"/>
</com.example.qwe.ScrollerLayout>
<Button
android:id="@+id/btn_go_0"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="Go To first child view"/>
<Button
android:id="@+id/btn_go_1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="Go To second child view"/>
<Button
android:id="@+id/btn_go_2"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="Go To third child view"/>
</LinearLayout>
可能遇见问题:
实际操作过程中我们可能会遇见ACTION_MOVE事件无法触发的现象,这是因为子View没有处理ACTION_DOWN的原因:https://blog.csdn.net/dreamsever/article/details/53907691
参考博客:
Scroller原理: https://www.jianshu.com/p/543b88fa609c
Scroller使用:https://blog.csdn.net/guolin_blog/article/details/48719871
下面是一个防QQ滑动删除控件的实现,如果在RecycleView中使用的话,需要调用setDragListener,处理Drag实现:
drag_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp">
<com.test.demo.DragControl
android:id="@+id/drag_item"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_height="match_parent"
android:id="@+id/item_text"
android:singleLine="true"
android:layout_width="match_parent"
android:clickable="true"
android:background="@color/colorPrimary"/>
<Button android:layout_height="match_parent"
android:id="@+id/item_button"
android:text="Del"
android:layout_width="200dp"
android:background="#ff00ff"/>
</com.test.demo.DragControl>
</LinearLayout>
DragControl.java
package com.test.demo;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.Scroller;
import androidx.annotation.Nullable;
public class DragControl extends ViewGroup {
public interface IDragEvent{
void onDragEnd(DragControl dragControl,boolean bDrag);
void onDragBegin(DragControl dragControl);
void onDragCancel(DragControl dragControl);
}
static public class DefaultDragEvent implements IDragEvent{
private DragControl dragControl = null;
@Override
public void onDragEnd(DragControl dragControl, boolean bDrag) {
if(bDrag){
this.dragControl = dragControl;
}
else{
this.dragControl = null;
}
}
@Override
public void onDragBegin(DragControl dragControl) {
if(this.dragControl != null) {
this.dragControl.reset();
this.dragControl = null;
}
}
@Override
public void onDragCancel(DragControl dragControl) {
if(this.dragControl != null) {
this.dragControl.reset();
this.dragControl = null;
}
}
}
private IDragEvent iDragEvent = null;
//滚动条
private Scroller mScroller;
//按住移动的时候屏幕坐标
private float mXMove;
//按下的时候屏幕坐标
private float mXDown = 0;
//最后移动的Move坐标
private float mXLastMove = 0;
//判定为移动的最小像素
private int mTouchSlop = 0;
//UI边界
private int mLeftBorder = 0;
private int mRightBorder = 0;
//是否开始drag
private boolean mbDrag = false;
public DragControl(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
mTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int nChildeCount = getChildCount();
for (int i = 0;i < nChildeCount;++i){
View child = getChildAt(i);
if(child.getVisibility() != View.GONE){
measureChild(child,widthMeasureSpec,heightMeasureSpec);
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int nChildCount = getChildCount();
int nOffsetLeft = 0;
for (int i = 0;i < nChildCount;++i){
View child = getChildAt(i);
child.layout(nOffsetLeft, 0, nOffsetLeft + child.getMeasuredWidth(), child.getMeasuredHeight());
nOffsetLeft += child.getMeasuredWidth();
}
if(nChildCount > 0){
mLeftBorder = getChildAt(0).getLeft();
mRightBorder = nOffsetLeft;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("kcc","onInterceptTouchEvent action:" + String.valueOf(ev.getAction()));
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mbDrag = false;
mXLastMove = mXDown = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
mXMove = ev.getRawX();
float diff = Math.abs(mXMove - mXDown);
if(diff >mTouchSlop){
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
if(iDragEvent != null)
iDragEvent.onDragCancel(this);
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("kcc","onTouchEvent action:" + String.valueOf(event.getAction()));
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
if(!mbDrag){
mbDrag = true;
if(iDragEvent != null)
iDragEvent.onDragBegin(this);
}
mXMove = event.getRawX();
int xOffset = (int)(mXLastMove - mXMove);
//左右边界判断
if(getScrollX() + xOffset < mLeftBorder){
scrollTo(mLeftBorder,0);
return true;
}
else if(getScrollX() + getWidth() + xOffset > mRightBorder){
scrollTo(mRightBorder - getWidth(),0);
return true;
}
scrollBy(xOffset,0);
mXLastMove = mXMove;
break;
case MotionEvent.ACTION_UP:
ViewParent parent = getParent();
//计算当前在哪个控件位置
int nOffsetX = Math.abs(getScrollX()) + getWidth();
int nOffsetRight = 0,targetIndex = 0,lastchildWidth = 0;
int nChildCount = getChildCount();
for (int i = 0;i < nChildCount;++i){
View child = getChildAt(i);
lastchildWidth = child.getMeasuredWidth();
//如果当前控件滑出距离大于1/2则显示当前滑出控件,否则显示前面一个控件
if(nOffsetX <= nOffsetRight + lastchildWidth){
if(nOffsetX <= nOffsetRight + lastchildWidth/2){
targetIndex = Math.max(0,i -1);
}
else{
targetIndex = i;
nOffsetRight += lastchildWidth;
}
break;
}
nOffsetRight += lastchildWidth;
}
int dx = nOffsetRight - getWidth() - getScrollX();
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
if(iDragEvent != null)
iDragEvent.onDragEnd(this,targetIndex != 0);
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
public void reset(){
scrollTo(mLeftBorder,0);
}
public void setDragListener(IDragEvent iDragEvent){
this.iDragEvent = iDragEvent;
}
}
调用:
//dragEvent属于Adapter成员变量
DragControl.IDragEvent dragEvent = new DragControl.DefaultDragEvent()
drag_item = itemView.findViewById(R.id.drag_item);
drag_item.setDragListener(IDragEvent);