使用Android造了个滚轮控件轮子示例
作者:奋斗的Leo 发布时间:2023-04-29 07:09:17
关于 Android 实现 iOS 上的滚轮选择效果的控件,到 github 上一搜一大堆,之所以还要造这个轮子,目的是为了更好的学习自定义控件,这个控件是几个月前写的了,经过一段时间的完善,现在开源,顺便写这一篇简单的介绍文章。
效果如下,录屏软件看起来可能有点卡顿,具体可以下载源码运行:
自定义控件无非是 measure,draw,layout 三个过程,如果要支持手势动作,那么就再加上 touch 。
measure
测量过程比较简单,以文本大小所需要的尺寸,再加上 padding。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wantWith = getPaddingLeft() + getPaddingRight();
int wantHeight = getPaddingTop() + getPaddingBottom();
calculateTextSize();
wantWith += mTextRect.width();
//可见 item 数量计算文本尺寸
if (mVisibilityCount > 0) {
wantHeight += mTextRect.height() * mVisibilityCount;
} else {
wantHeight += mTextRect.height() * DEFALUT_VISIBILITY_COUNT;
}
setMeasuredDimension(
resolveSize(wantWith, widthMeasureSpec),
resolveSize(wantHeight, heightMeasureSpec)
);
mNeedCalculate = true;
}
draw
绘制过程是通过 canvas 的位移去绘制不同位置的部件,包括文本内容和选择框之类的,这里可能需要注意下的地方是,不要一次性把所有文本绘制出来,只需要绘制可见文本即可。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (hasDataSource()) {
// 省略
// 这里计算下需要绘制的数量,+2 只是确保不会出现空白
final int drawCount = mContentRect.height() / mTextRect.height() + 2;
int invisibleCount = 0;
int dy = -mDistanceY;
// 省略
// 通过 translate 绘制文本
for (int i = 0; (i < drawCount && mDataSources.size() > (invisibleCount + i));
i++) {
final int position = invisibleCount + i;
String text = mDataSources.get(position);
if (i > 0) {
canvas.translate(0, mTextRect.height());
}
final PointF pointF = calculateTextGravity(text);
mTextPaint.setTextSize(mTextSize);
if (position == selctPosition) {
mTextPaint.setColor(mSelectedTextColor);
} else {
mTextPaint.setColor(mNormalTextColor);
}
canvas.drawText(text, pointF.x, pointF.y, mTextPaint);
}
canvas.restoreToCount(saveCount);
}
// 绘制选择框
int saveCount = canvas.save();
mDrawPaint.setColor(mSelectedLineColor);
canvas.translate(mContentRect.left, mContentRect.top);
canvas.drawLine(
mSelctedRect.left,
mSelctedRect.top,
mSelctedRect.right,
mSelctedRect.top,
mDrawPaint
);
canvas.drawLine(
mSelctedRect.left,
mSelctedRect.bottom,
mSelctedRect.right,
mSelctedRect.bottom,
mDrawPaint
);
canvas.restoreToCount(saveCount);
}
layout
因为这个控件是继承于 View,所以不需要处理 onLayout。
touch
如果对 touch event 分发流程熟悉的话,那么很多处理可以说是模版代码,可以参考 NestedScrollView、ScrollView。
在 onInterceptTouchEvent 中,判断是否开始进行拖动手势,保存到变量(mIsBeingDragged)中:
// 多指处理
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
// 开始拖动
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
// 禁止父控件拦截事件分发
parent.requestDisallowInterceptTouchEvent(true);
}
}
在 onTouchEvent 中对 ACTION_MOVR 进行拖动的处理,如果支持嵌套滚动,那么会预先进行嵌套滚动的分发。如果支持阴影效果,那么使用 EdgeEffect。
// 和 onInterceptTouchEvent 一样进行拖动手势开始的判断
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// 拖动处理
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollBy will call onOverScrolled, which
// calls onScrollChanged if applicable.
// 滚动处理,overScrollBy 中会处理嵌套滚动预先分发
if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
&& !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = mScrollY - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
// 嵌套滚动
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
final int pulledToY = oldY + deltaY;
// 拖动阴影效果
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
postInvalidateOnAnimation();
}
}
}
支持滚动手势的控件,一般都会支持 fling 手势,可以理解为惯性滚动。这也是模版代码,在 onTouchEvent 中对 ACTION_UP 中对拖动速度进行分析。
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
// 获取拖动速度
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
// 可以进行 fling 操作
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
具体的代码可以在 ScrollView 中阅读。
回到我实现的自定义控件来,对 touch event 的处理代码可以说是和系统控件的处理没有什么两样,在获取到拖动的距离后,根据这个值绘制不同位置的可见区域。这里多了两个处理是:
第一拖动结束后,进行复位处理。拖动结束后,选择框如果停留在两个 item 之间,那么根据和两个 item 的距离进行比较,选择更近的 item。
private void correctionDistanceY() {
if (mDistanceY % mTextRect.height() != 0) {
int position = mDistanceY / mTextRect.height();
int remainder = mDistanceY % mTextRect.height();
if (remainder >= mTextRect.height() / 2f) {
position++;
}
int newDistanceY = position * mTextRect.height();
animChangeDistanceY(newDistanceY);
}
}
第二个是在使用上发现的问题,如果剩余可滚动的距离过短,拖动的手势速度又很快,就会导致 fling 处理没结束,视觉上又没有改变,同时是在滚动结束后才进行选择的回调,所以体检上不好,但是 Scroller 并没有提供 setDuration,所以拷贝 Scroller 中计算 duration 的方法,根据剩余的滚动计算合适的 duration,手动中断 Scroller 的 fling 处理。
if ((SystemClock.elapsedRealtime() - mStartFlingTime) >= mFlingDuration || currY == mScroller.getFinalY()) {
//duration or current == final
if (DEBUG) {
Logger.d("abortAnimation");
}
mScroller.abortAnimation();
}
具体的代码可以阅读源码。
来源:https://juejin.im/post/5a8e84045188257a76633a6c
猜你喜欢
- 一、Maven生命周期、阶段、目标 &nbs
- 经常要检测某些IP地址范围段的计算机是否在线。有很多的方法,比如进入到网关的交换机上去查询、使用现成的工具或者编写一个简单的DOS脚本等等,
- 目录对zygote的理解作用启动流程启动入口脚本讲解启动过程App_main::mainAndroidRuntime::start对zygo
- 文件上传是开发中十分常见的功能,在servlet3.0之前,实现文件上传需要使用一些插件技术,比如:commons-fileuploadsm
- 条形码,是由宽度不等的多个黑条和空白所组成,用以表达一组信息的图形标识符。通过给文档添加条形码,可以直观,快捷地访问和分享一些重要的信息。本
- 学过Spring的小伙伴对于IOC一定不陌生,IOC:控制反转(Inversion of Control,英文缩写为IoC)是一个重要的面向
- 将某个项目从Spring Boot1升级Spring Boot2之后出现如下报错,查了很多不同的解决方法都没有解决:Spring boot2
- 写作原因:跨进程通信的实现和理解是Android进阶中重要的一环。下面博主分享IPC一些相关知识、操作及自己在学习IPC过程中的一些理解。这
- 一、Maven聚合开发_继承关系 Maven中
- 定时器问题定时器属于基本的基础组件,不管是用户空间的程序开发,还是内核空间的程序开发,很多时候都需要有定时器作为基础组件的支持。一个定时器的
- 抽象类和抽象方法常用知识点:(1)抽象类作为被继承类,子类必须实现抽象类中的所有抽象方法,除非子类也为抽象类。也就是说,如果子类也为抽象类,
- 数组实现Java 自定义Queue队列及应用Java 自定义队列Queue:队列的抽象数据类型就是一个容器,其中的对象排成一个序列,我们只能
- 本文旨在用通俗的语言讲述枯燥的知识定时任务作为一种系统调度工具,在一些需要有定时作业的系统中应用广泛,如每逢某个时间点统计数据、在将来某个时
- Android自带的跑马灯效果不太好控制,还必须要满足条件才能有效果,而且速度不受控制。前面我的博客中有一篇就是用Android自带的跑马灯
- 首先定义两个示例类ClassA,ClassB,用于后续的示例演示package cn.lzrabbit;public class Class
- 1.新建一个项目2.给项目添加引用:Microsoft Excel 12.0 Object Library (2007版本)using Ex
- Android 消息机制1.概述Android应用启动时,会默认有一个主线程(UI线程),在这个线程中会关联一个消息队列(MessageQu
- Mybatis入门-基于配置实现单表的增删改查Mybatis简介官网链接:https://mybatis.org/mybatis-3/zh/
- 不多说废话,直接进入主菜!!步骤:1.搭建SpringBoot的开发环境,略(有不会的可以私信我)。2.编写一个自定义异常,自定义异常需要继
- --DateTime 数字型 System.DateTime currentTime=new System.DateTime(); 1.1