使用Android造了个滚轮控件轮子示例

关于 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();
     }

具体的代码可以阅读源码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android 实现IOS 滚轮选择控件的实例(源码下载)

     Android 实现IOS 滚轮选择控件的实例 最近根据项目需要,整理了一个相对比较全面的 WheelView 使用控件,借用之前看到的一句话来说,就是站在巨人肩膀上,进行了一些小调整. 这里先贴上效果图 一般常用的时间选择格式,,单项选择,以及城市联动,这里基本都可以满足了. 这里把 单项选择,和 日期时间选择 给提出到 Util 类中,代码如下: public class Util { /** * 时间选择回调 */ public interface TimerPickerCallBack

  • android仿iphone滚轮控件显示效果

    android仿iphone滚轮控件显示效果,供大家参考,具体内容如下 在论坛里看到的,自己弄个效果: 这个滚动的WheelView /* * Android Wheel Control. * https://code.google.com/p/android-wheel/ * * Copyright 2010 Yuri Kanivets * * Licensed under the Apache License, Version 2.0 (the "License"); * you

  • Android高仿IOS 滚轮选择控件

    最近根据项目需要,整理了一个相对比较全面的 WheelView 使用控件,借用之前看到的一句话来说,就是站在巨人肩膀上,进行了一些小调整. 这里先贴上效果图 一般常用的时间选择格式,,单项选择,以及城市联动,这里基本都可以满足了. 这里把 单项选择,和 日期时间选择 给提出到 Util 类中,代码如下: public class Util { /** * 时间选择回调 */ public interface TimerPickerCallBack { void onTimeSelect(Stri

  • Android滚轮选择时间控件使用详解

    滚轮选择控件 Android自带的选择时间控件有点丑,往往产品和设计都比较嫌弃,希望做成ios一样的滚轮选择,下面是我在NumberPicker的基础上自定义的选择控件,效果如下: 原理 基于NumberPicker实现 动态填充数值 联动 接口监听回调 实现滚轮效果有github上mark比较多的WheelView,但是阅读源码发现数据是一次性填入的,选择时间的话,填入10年就是10*365=3650条数据,也就是new出三千多个TextView,想想都觉得恐怖,肯定是不行的,于是便想到用Nu

  • Android自定义实现循环滚轮控件WheelView

    首先呈上Android循环滚轮效果图: 现在很多地方都用到了滚轮布局WheelView,比如在选择生日的时候,风格类似系统提供的DatePickerDialog,开源的控件也有很多,不过大部分都是根据当前项目的需求绘制的界面,因此我就自己写了一款比较符合自己项目的WheelView. 首先这个控件有以下的需求:  1.能够循环滚动,当向上或者向下滑动到临界值的时候,则循环开始滚动  2.中间的一块有一块半透明的选择区,滑动结束时,哪一块在这个选择区,就选择这快.  3.继承自View进行绘制 然

  • 轻松实现可扩展自定义的Android滚轮时间选择控件

    项目需求中有个功能模块需要用到时间选择控件,但是android系统自带的太丑了,只能自己优化下,结合WheelView实现滚轮选择日期,好像网上也挺多这种文章的.但是适用范围还是不同,希望这个能够对需求相同的朋友有一定帮助.控件标题还有年月日时分秒这些可以自己控制是否显示,先来看效果. 1.有年月日时分的开始时间 2.只有年月日的结束时间 3.用于有时身份证到期的时间选择(分为勾选长期和直接选择时间两种,另外长期后面自己也可以进行扩展) 4.项目结构 5.直接贴代码,代码里面注释很详细 <spa

  • 使用Android造了个滚轮控件轮子示例

    关于 Android 实现 iOS 上的滚轮选择效果的控件,到 github 上一搜一大堆,之所以还要造这个轮子,目的是为了更好的学习自定义控件,这个控件是几个月前写的了,经过一段时间的完善,现在开源,顺便写这一篇简单的介绍文章. 效果如下,录屏软件看起来可能有点卡顿,具体可以下载源码运行: 自定义控件无非是 measure,draw,layout 三个过程,如果要支持手势动作,那么就再加上 touch . measure 测量过程比较简单,以文本大小所需要的尺寸,再加上 padding. @O

  • Android开发自定义实时图表控件实现示例

    目录 概述 演示 环境 实现 第一步:新建项目RealTimeChartDemo 第二步:新建RealTimeChart类 第三步:添加自定义变量 第四步:初始化基础参数 第五步:初始化宽高等参数 第六步:定义添加坐标点方法 第七步:定义坐标偏移方法 第八步:定义绘制网格线方法 第九步:定义绘制X轴标签方法 第十步:定义绘制坐标点方法 第十一步:绘制 第十二步:在activity_main.xml中使用控件 第十三步:在MainActivity中模拟添加数据点 概述 有时我们需要实时滚动显示一段

  • android开发教程之switch控件使用示例

    复制代码 代码如下: <Switchandroid:id="@+id/open"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textOff="蓝牙关闭中"android:textOn="蓝牙开启中" /> 复制代码 代码如下: open.setOnCheckedChangeListe

  • android应用开发之spinner控件的简单使用

    Android的控件有很多种,其中就有一个Spinner的控件,这个控件其实就是一个下拉显示列表.Spinner是位于 android.widget包下的,每次只显示用户选中的元素,当用户再次点击时,会弹出选择列表供用户选择,而选择列表中的元素同样来自适配器.Spinner是View类的一个子类. 先看spinner的效果图: 代码: MainActivity package com.mecury.spinnertest; import java.util.ArrayList; import a

  • 分享Android仿刮奖效果控件

    本文实例为大家分享了Android刮刮卡效果控件,供大家参考,具体内容如下 刮刮卡类: package com.reyo.view; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Paint; import android.gr

  • Android编程开发之TextView控件用法(2种方法)

    本文实例讲述了Android编程开发之TextView控件用法.分享给大家供大家参考,具体如下: 这里我们会讲讲常用控件的使用. 在今后的大多数章节里面也是一样的,我们会具体的说说某些控件的用法.因为只要把这些控件组合在一起它们就是一个应用了. 好吧我们直接看看这个控件怎么用. 细心的同学会发现,其实这个控件的内容是定义在values文件夹里面的strings.xml中的. 那么我们只需要给它加一段代码: 复制代码 代码如下: <string name="test">Wel

  • Android开发之瀑布流控件的实现与使用方法示例

    本文实例讲述了Android开发之瀑布流控件的实现与使用方法.分享给大家供大家参考,具体如下: public class FlowLayout extends ViewGroup { /**行里子view之间的行距离*/ public int mHorizontolSpace = Util.getDimen(R.dimen.top_padding); /**行里子view之间的垂直距离*/ public int mVerticalSpace = Util.getDimen(R.dimen.top

  • Android开发中自定义ProgressBar控件的方法示例

    本文实例讲述了Android开发中自定义ProgressBar控件的方法.分享给大家供大家参考,具体如下: 很简单,首先加载Drawable,在onMeasure设置好其区域大小, 然后使用canvas.clipRect绘图 public class ProgressView extends ImageView { private Drawable maskDraw; /** * 加载的进度 0-100 */ private int mProcess = 20; public ProgressV

随机推荐