目錄 |
---|
簡介 |
功能 |
主要程式碼介紹 |
如何使用 |
ScrollMenu全部程式碼 |
專案地址 |
總結 |
圖紙 |
簡介
- 這個自定義的view,繼承RelativeLayout(原因現在大部分父佈局用的都是RelativeLayout)
- 通過Scroller實現滑動
- 通過速度跟蹤器獲取滑動速度
- 通過設定子控制元件
tag
排除特殊情況
功能
- 實現右滑隱藏
- 上下滑動切換顯示資料的監聽(在監聽中更換資料)
- 排除了RecyclerView垂直和水平滑動和ScrollMenu的衝突
- 通過為子佈局設定特定的tag解決衝突(因為還有ScrollView等沒有加入判斷,需要自行設定tag排除衝突)
- 可以設定是否能水平滑動或是否能垂直方向滑動
主要程式碼介紹
private boolean
canVerticalSlide, //能否垂直方向滑動
canHorizontalSlide,//能否水平方向滑動
openVerticalSlide = true,//開啟垂直方向滑動
openHorizontalSlide = true;//開啟水平方向的滑動
複製程式碼
- 在ScrollMenu中通過條件判斷此時是否正水平和垂直滑動
canHorizontalSlide、canVerticalSlide
,通過控制這兩個來控制能否滑動 - 通過
openVerticalSlide、openHorizontalSlide
在activity中呼叫這兩個變數的set
方法,來間接控制canHorizontalSlide、canVerticalSlide
的值
double angle = Math.atan2(Math.abs(ev.getY() - angleLastY), Math.abs(ev.getX() - angleLastX)) * 180 / Math.PI;
複製程式碼
- 計算滑動的角度
canHorizontalSlide = canHorizontalSlide && angle < 30;
canVerticalSlide = canVerticalSlide && angle > 30;
複製程式碼
- 如果角度小於30°則水平能滑動,垂直方向不能滑動
- 如果角度大於30°則垂直能滑動,水平方向不能滑動
/**
* 計算(x, y)座標是否在child view的範圍內
*
* @param child 子佈局
* @param x x座標
* @param y y座標
* @return 子佈局是否在點選範圍內
*/
public boolean isTouchPointInView(View child, int x, int y) {
int[] location = new int[2];
child.getLocationOnScreen(location);
int top = location[1];
int left = location[0];
int right = left + child.getMeasuredWidth();
int bottom = top + child.getMeasuredHeight();
return y >= top && y <= bottom && x >= left && x <= right;
}
複製程式碼
- 計算點選(x,y)座標是否在此子佈局範圍之內
View view = getTargetView(this, (int) ev.getRawX(), (int) ev.getRawY());
複製程式碼
- 獲取點選位置的佈局(只獲取RecyclerView、或設定了
tag:no_vertical、no_horizontal
的佈局)
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
$e(String.format("computeScroll mScroller --- currX:%d --- currY:%d", mScroller.getCurrX(), mScroller.getCurrY()));
if (-getHeight() == mScroller.getCurrY()) {
mScrollHandler.sendEmptyMessage(ScrollHandler.FAST_BOTTOM_TO_NORMAL);
}
if (getHeight() == mScroller.getCurrY()) {
mScrollHandler.sendEmptyMessage(ScrollHandler.FAST_TOP_TO_NORMAL);
}
invalidate();
}
}
複製程式碼
- 當Scroller呼叫
startScroll
方法後,會不斷的呼叫computeScroll
通過不斷的呼叫scrollTo
高頻率的重新整理顯示試圖 - 當
(-getHeight() == mScroller.getCurrY())
為true
表示滑出底部 - 當
getHeight() == mScroller.getCurrY()
為true
表示為滑出頂部
public void toRight() {
status = RIGHT;
$e("toRight getScrollX = " + getScrollX());
mScroller.startScroll(getScrollX(), 0, -(getWidth() + getScrollX()), 0, 1000);
invalidate();
}
public void toTop() {
status = TOP;
mScroller.startScroll(0, getScrollY(), 0, -getScrollY() + getHeight(), 1000);
invalidate();
}
public void toBottom() {
status = BOTTOM;
mScroller.startScroll(0, getScrollY(), 0, -(getHeight() + getScrollY()), 1000);
invalidate();
}
public void toNormal() {
if (status == TOP || status == BOTTOM) {
mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 1000);
} else {
$e("toLeft getScrollX = " + getScrollX());
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 1000);
}
invalidate();
status = NORMAL;
}
複製程式碼
- toRight() :向右滑動
- toTop():向頂部外滑動
- toBottom():向底部外滑動
- toNormal(): 向正常顯示狀態滑動
如何使用
- 監聽上下滑動完成後的事件監聽(用來更新顯示的資料)
scrollMenu.setOnScrollCompleteListener(new ScrollMenu.OnScrollCompleteListener() {
@Override
public void completeTop() {
Toast.makeText(MainActivity.this, "↑↑上滑切換↑↑", Toast.LENGTH_SHORT).show();
changeData(true);
}
@Override
public void completeBottom() {
Toast.makeText(MainActivity.this, "↓↓下滑切換↓↓", Toast.LENGTH_SHORT).show();
changeData(false);
}
});
複製程式碼
- 開啟或關閉橫向縱向滑動,示例程式碼如下
ctvH.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ctvH.toggle();
ctvH.setText(ctvH.isChecked() ? "橫向滑動開" : "橫向滑動關");
scrollMenu.setOpenHorizontalSlide(ctvH.isChecked());
}
});
ctvV.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ctvV.toggle();
ctvV.setText(ctvV.isChecked() ? "縱向滑動開" : "縱向滑動關");
scrollMenu.setOpenVerticalSlide(ctvV.isChecked());
}
});
複製程式碼
- 需要解決滑動時的衝突(RecyclerView解決了水平和垂直情況可不用考慮),為子控制元件設定
tag
,例如下面的ScrollView(垂直方向不滑動no_vertical
, 水平方向不滑動no_horizontal
)
<ScrollView
android:id="@+id/scrollView"
android:layout_width="100dp"
android:layout_height="match_parent"
android:layout_below="@id/rvHorizontal"
android:tag="no_vertical"
android:background="@android:color/holo_red_light">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="sdjaflkjsdlakfjlknsdfjsadljfldsjafnsdfjfdsadfsadfsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafoijoijsoadifnoisdajofihosadhfoihsoidfnoisadhgiouasho;eiwnfoiewahfioaewboeifwbgwoeibfoieawbngfiownfdsafsj" />
</ScrollView>
複製程式碼
ScrollMenu全部程式碼
package com.example.jiana.scrollmenudemo;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.Scroller;
import java.lang.ref.WeakReference;
import java.util.Timer;
import java.util.TimerTask;
public class ScrollMenu extends RelativeLayout {
private boolean isOpenLog = true;//是否開啟log
/**
* 設定tag為"no_horizontal"的子佈局觸控無法水平滑動
*/
private static final String VIEW_TAG_NO_VERTICAL = "no_vertical";
/**
* 設定tag為"no_vertical"的子佈局觸控無法垂直滑動
*/
private static final String VIEW_TAG_NO_HORIZONTAL = "no_horizontal";
/**
* 正常狀態
*/
public static final int NORMAL = 0;
/**
* 側滑到頂部
*/
public static final int TOP = 2;
/**
* 滑到右側
*/
public static final int RIGHT = 3;
/**
* 側滑到底部
*/
public static final int BOTTOM = 4;
private static final String TAG = "ScrollMenu";
//滑動元件
private Scroller mScroller;
//數度跟蹤者
private VelocityTracker mVelocityTracker;
//最後一個動作的位置
private float mLastTouchX, mLastTouchY;
//能被拖動的臨界值
private int mTouchSlop;
//滑動的最大速度
private int mMaximumVelocity;
private float angleLastX, angleLastY;
//拖動鎖
private boolean mDragging = false;
private boolean
canVerticalSlide, //能否垂直方向滑動
canHorizontalSlide,//能否水平方向滑動
openVerticalSlide = true,//開啟垂直方向滑動
openHorizontalSlide = true;//開啟水平方向的滑動
/**
* 當前狀態
*/
private int status = NORMAL;
private ScrollHandler mScrollHandler;
public ScrollMenu(Context context) {
super(context);
init(context);
}
public ScrollMenu(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ScrollMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mScrollHandler = new ScrollHandler(this);
mScroller = new Scroller(context);
mVelocityTracker = VelocityTracker.obtain();
//獲取系統觸控的臨界常量值
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
$e(String.format("computeScroll mScroller --- currX:%d --- currY:%d", mScroller.getCurrX(), mScroller.getCurrY()));
if (-getHeight() == mScroller.getCurrY()) {
mScrollHandler.sendEmptyMessage(ScrollHandler.FAST_BOTTOM_TO_NORMAL);
}
if (getHeight() == mScroller.getCurrY()) {
mScrollHandler.sendEmptyMessage(ScrollHandler.FAST_TOP_TO_NORMAL);
}
invalidate();
}
}
/**
* 初始化滾動和開始繪製
*/
public void toRight() {
status = RIGHT;
$e("toRight getScrollX = " + getScrollX());
mScroller.startScroll(getScrollX(), 0, -(getWidth() + getScrollX()), 0, 1000);
invalidate();
}
public void toTop() {
status = TOP;
mScroller.startScroll(0, getScrollY(), 0, -getScrollY() + getHeight(), 1000);
invalidate();
}
public void toBottom() {
status = BOTTOM;
mScroller.startScroll(0, getScrollY(), 0, -(getHeight() + getScrollY()), 1000);
invalidate();
}
public void toNormal() {
if (status == TOP || status == BOTTOM) {
mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 1000);
} else {
$e("toLeft getScrollX = " + getScrollX());
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 1000);
}
invalidate();
status = NORMAL;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
canHorizontalSlide = openHorizontalSlide;
canVerticalSlide = openVerticalSlide;
View view = getTargetView(this, (int) ev.getRawX(), (int) ev.getRawY());
$e("dispatchTouchEvent view = " + view);
if (view != null) {
if (view instanceof RecyclerView) {
RecyclerView rv = (RecyclerView) view;
canHorizontalSlide = openHorizontalSlide && !rv.getLayoutManager().canScrollHorizontally();
canVerticalSlide = openVerticalSlide && !canHorizontalSlide;
} else if (VIEW_TAG_NO_VERTICAL.equals(view.getTag())) {
canHorizontalSlide = openHorizontalSlide;
canVerticalSlide = false;
} else if (VIEW_TAG_NO_HORIZONTAL.equals(view.getTag())) {
canHorizontalSlide = false;
canVerticalSlide = openVerticalSlide;
}
$e("dispatchTouchEvent canHorizontalSlide = " + canHorizontalSlide);
$e("dispatchTouchEvent " +
"canVerticalSlide = " + canVerticalSlide);
}
if (onTouchDownListener != null) {
onTouchDownListener.touch(ev);
}
}
return super.dispatchTouchEvent(ev);
}
/**
* 監聽向子佈局傳遞的觸控事件和攔截事件
* 如果子佈局是互動式的(如button),將仍然能接收到觸控事件
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
$e(String.format("onInterceptTouchEvent action = %d, x = %f, y = %f", ev.getAction(), ev.getX(), ev.getY()));
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//判斷是否已經完成滾動,如果滾動則停止
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
//重置速度跟蹤器
mVelocityTracker.clear();
mVelocityTracker.addMovement(ev);
//儲存初始化觸控位置
mLastTouchX = ev.getX();
mLastTouchY = ev.getY();
angleLastX = ev.getX();
angleLastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
final float x = ev.getX();
final float y = ev.getY();
final int xDiff = (int) Math.abs(x - mLastTouchX);
final int yDiff = (int) Math.abs(y - mLastTouchY);
$e("onInterceptTouchEvent xDiff = " + xDiff);
$e("onInterceptTouchEvent yDiff = " + yDiff);
//計算角度
double angle = Math.atan2(Math.abs(ev.getY() - angleLastY), Math.abs(ev.getX() - angleLastX)) * 180 / Math.PI;
//驗證移動距離是否足夠成為觸發拖動事件
if (xDiff > mTouchSlop || yDiff > mTouchSlop) {
canHorizontalSlide = canHorizontalSlide && angle < 30;
canVerticalSlide = canVerticalSlide && angle > 30;
if (!canVerticalSlide && !canHorizontalSlide) {
return super.onInterceptTouchEvent(ev);
}
mDragging = true;
mVelocityTracker.addMovement(ev);
$e("onInterceptTouchEvent 獲取這個動作事件");
//獲取這個事件
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
mDragging = false;
mVelocityTracker.clear();
break;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 處理接收的事件(事件由onInterceptTouchEvent獲取)
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
$e(String.format("onTouchEvent action = %d, x = %f, y = %f", event.getAction(), event.getX(), event.getY()));
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//獲取後續事件
return true;
case MotionEvent.ACTION_MOVE:
move(event);
break;
case MotionEvent.ACTION_CANCEL:
mDragging = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_UP:
mDragging = false;
//計算當前的速度,如果速度大於最小數度臨界值則開啟一個滑動
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityX = (int) mVelocityTracker.getXVelocity();
int velocityY = (int) mVelocityTracker.getYVelocity();
$e("onTouchEvent MotionEvent.ACTION_UP velocityX = " + velocityX);
$e("onTouchEvent MotionEvent.ACTION_UP velocityY = " + velocityY);
$e("onTouchEvent getScrollX() = " + getScrollX());
$e("onTouchEvent getScrollY() = " + getScrollY());
if (canHorizontalSlide) {
if (velocityX >= 5000 || (velocityX >= 0 && getScrollX() <= -getWidth() / 3) || (velocityX < 0 && velocityX > -5000 && getScrollX() < -getWidth() * 2 / 3)) {
toRight();
} else {
toNormal();
}
} else if (canVerticalSlide) {
if (velocityY >= 5000 || (velocityY >= 0 && getScrollY() <= -getHeight() / 4)) {
toBottom();
break;
}
if ((velocityY < -5000 && status == NORMAL) || (velocityY < 0 && getScrollY() >= getHeight() / 4)) {
toTop();
break;
}
toNormal();
}
break;
}
return super.onTouchEvent(event);
}
/**
* 處理移動事件
*/
private void move(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
//水平滾動距離
float diffX = mLastTouchX - x;
//垂直方向滑動的距離
float diffY = mLastTouchY - y;
//如果可以拖動是否被鎖,x與y移動的距離大於可移動的距離
$e("onTouchEvent mDragging = " + mDragging);
if (!mDragging && (Math.abs(diffX) > mTouchSlop || Math.abs(diffY) > mTouchSlop)) {
mDragging = true;
}
//計算角度
double angle = Math.toDegrees(Math.atan2(Math.abs(y - angleLastY), Math.abs(x - angleLastX)));
$e("onTouchEvent angle = " + angle);
if (mDragging) {
//滑動這個view
if (canHorizontalSlide && angle < 30) {
scrollBy((int) diffX, 0);
mLastTouchX = x;
canVerticalSlide = false;
} else if (canVerticalSlide && angle > 30) {
scrollBy(0, (int) diffY);
mLastTouchY = y;
canHorizontalSlide = false;
}
}
}
/**
* 根據觸控到文字獲得具體的子view
*/
public View getTargetView(View view, int x, int y) {
View target = null;
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0, len = viewGroup.getChildCount(); i < len; i++) {
View child = viewGroup.getChildAt(i);
if (child instanceof RecyclerView) {
target = isTouchPointInView(child, x, y) ? child : null;
if (target != null) {
break;
}
} else if (child instanceof ViewGroup) {
View v = getTargetView(child, x, y);
if (v != null) {
return v;
}
}
target = (isTouchPointInView(child, x, y) && (VIEW_TAG_NO_VERTICAL.equals(child.getTag()) || VIEW_TAG_NO_HORIZONTAL.equals(child.getTag()))) ? child : null;
if (target != null) {
break;
}
}
return target;
}
/**
* 計算(x, y)座標是否在child view的範圍內
*
* @param child 子佈局
* @param x x座標
* @param y y座標
* @return 子佈局是否在點選範圍內
*/
public boolean isTouchPointInView(View child, int x, int y) {
int[] location = new int[2];
child.getLocationOnScreen(location);
int top = location[1];
int left = location[0];
int right = left + child.getMeasuredWidth();
int bottom = top + child.getMeasuredHeight();
return y >= top && y <= bottom && x >= left && x <= right;
}
public int getStatus() {
return status;
}
private OnScrollCompleteListener onScrollCompleteListener;
private OnTouchDownListener onTouchDownListener;
public void setOnTouchDownListener(OnTouchDownListener l) {
this.onTouchDownListener = l;
}
public void setOnScrollCompleteListener(OnScrollCompleteListener l) {
this.onScrollCompleteListener = l;
}
public interface OnScrollCompleteListener {
void completeTop();
void completeBottom();
}
public interface OnTouchDownListener {
void touch(MotionEvent ev);
}
public void setOpenVerticalSlide(boolean openVerticalSlide) {
this.openVerticalSlide = openVerticalSlide;
}
public void setOpenHorizontalSlide(boolean openHorizontalSlide) {
this.openHorizontalSlide = openHorizontalSlide;
}
private static class ScrollHandler extends Handler {
/**
* 快速恢復正常模式
*/
public static final int FAST_TOP_TO_NORMAL = 0X12345;
public static final int FAST_BOTTOM_TO_NORMAL = 0X12346;
private WeakReference<ScrollMenu> wr;
private boolean isRun;
public ScrollHandler(ScrollMenu scrollMenu) {
wr = new WeakReference<>(scrollMenu);
}
@Override
public void handleMessage(Message msg) {
ScrollMenu mScrollMenu = wr.get();
if (mScrollMenu == null) {
return;
}
switch (msg.what) {
case FAST_BOTTOM_TO_NORMAL:
mScrollMenu.scrollTo(0, -mScrollMenu.getHeight());
mScrollMenu.invalidate();
mScrollMenu.scrollTo(0, 0);
if (mScrollMenu.onScrollCompleteListener != null && agreeOperated()) {
mScrollMenu.onScrollCompleteListener.completeBottom();
}
break;
case FAST_TOP_TO_NORMAL:
mScrollMenu.scrollTo(0, mScrollMenu.getHeight());
mScrollMenu.invalidate();
mScrollMenu.scrollTo(0, 0);
if (mScrollMenu.onScrollCompleteListener != null && agreeOperated()) {
agreeOperated();
mScrollMenu.onScrollCompleteListener.completeTop();
}
break;
}
}
/**
* 是否同意操作
*/
private boolean agreeOperated() {
if (isRun) {
return false;
}
isRun = true;
Timer tExit = new Timer();
tExit.schedule(new TimerTask() {
@Override
public void run() {
isRun = false;
}
}, 1000);
return true;
}
}
/**
* 列印log
*
* @param s 列印的log資料
*/
private void $e(String s) {
if (isOpenLog) {
Log.e(TAG, s);
}
}
}
複製程式碼
專案地址
github:https://github.com/xujiaji/ScrollMenuDemo
總結
- 總體上來看功能實現,正常流暢滑動
- 細節上需要考慮很多其他使用情況,如果沒有匯入RecyclerView會報錯,因為預設判斷排除了RecyclerView橫向和縱向的滑動