从零开始打造垂直SeekBar

从零开始打造垂直SeekBar

偶然中需要使用到垂直的seekbar控件,却发现居然原生没有提供,本来打算直接将控件进行简单的rotation,结果需求要自定义seekbar的样式,而样式是一张固定长宽和方向的图片,直接旋转有各种莫名其妙的问题,嫌麻烦就自己手写一个好了。顺便写个简单的功能性自定义View的教程。

市面上已经实现的垂直Seekbar

一开始是打算在网上找个实现好的直接使用的,然后发现大致实现都是一样,创建一个view类继承于原生seekbar,在onDraw中旋转画布(rotate),并且修正view的位置(translate)。最后重写onTouchEvent,修成progress等等,而且google官方实现中也是类似的方式(这里吐个槽,既然你都知道会有人用到垂直的了,还写了demo,干嘛不不直接提供出来)。当然以上只是核心内容,实际上很多实现的相当繁琐。

由于我本身的需求需要深度定制Seekbar,上面的思路对我来说有很大的局限性,而且网上的代码也是鱼龙混杂,就决定自己从头开始写了。

效果图

这里写图片描述

从零开始

之所以说从零开始,是因为我不打算借助google已经实现的seekbar或者progressbar进行继承封装,我会直接基于FrameLayout进行编写。没有从更底层的viewGroupview进行是因为本人比较懒,懒的去自己measurelayout了。

思路

简单拆分下需求,seekbar分为两部分,一个滑动轨道、一个拖拽点。将一个Layout看做成轨道,实际上我们需要的只是在Layout中添加一个子View来充当拖拽点就行了。然后重写onTouchListener判断当前滑动距离,并用translate等方式移动view,并且在移动时计算移动的progress。以下是核心代码:

  • 创建thumbView,手动设置LayoutParams,告诉轨道自己的布局属性。
        //显示部分
        View thumb = new View(getContext());
        thumb.setBackgroundColor(Color.WHITE);
        int thumbHeight = (int) PublicUtil.getInstance().dpToPx(getContext(), 4);
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, thumbHeight);
        layoutParams.topMargin = 5;
        layoutParams.bottomMargin = 5;
        thumb.setLayoutParams(layoutParams);
        addView(thumb);
  • 添加监听,这里说明一下,很多不熟悉的人会不知道监听事件应该添加到哪个布局,这里分为两种情况:
    • 一种是,全局可响应拖拽事件,也就是不用特意拖拽thumb也能达到效果,thumb的目的只是纯粹的将当前进度展示给用户。这种情况优点是简单粗暴,而且操作流畅,但是总感觉怪怪的,不知道的以为是出了bug,怎么明明没点到也能滑动。这种需要重写父布局即“轨道”的onTouch事件。
    • 另一种,就是普通的,必须拖拽thumb才能响应事件。优点是符合日常逻辑,缺点是操作比较繁琐,如果view面积太小很容易误触,说白了就是不容易点到。这种需要重写thumb的onTouch事件,同时,由于需要滑动,thumb的通常很小,滑动体验非常不好,这时候还是需要和父布局onTouch**配合使用**。
  • 这里采用的第二种方式,大致介绍下onTouch的配合使用,这是我自己总结出来的。
  • 首先先创建一个全局变量来存储点击状态,重写子view的点击事件,判断ACTION_DOWN事件,这时候将点击状态变更为true,注意重写的点击事件方法返回值必须faslse
  • 然后重写父布局的点击事件,判断当前点击状态,如果为true则开始监听滑动并更新布局,并且在ACTION_UP后就点击状态变更为false。如果对手势消费事件不清楚的可以看我的另一篇文章:Android 触摸事件传递
    //拖拽事件
    thumbView.setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                isTouch = true;
            }
            return false;
        }
    });

    private float lastY;
    private float transY = 0;
    private boolean isTouch = false;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isTouch) {
            return false;
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float changY = event.getY() - lastY;
                lastY = event.getY();
                transY += changY;
                moveThumb(transY);
                break;
            case MotionEvent.ACTION_UP:
                isTouch = false;
                break;
        }
        return true;
    }

以上主体功能就实现了。

补充功能

一个seekbar除了能够被用户拖动外,还需要将拖动的距离转化成对应的进度,结合需求我这里需要一个回调来通知当前的进度改变。

  • 计算进度

    我们需要计算初滑条能够被拖拽的总长度,得到总长度之后用当前拖拽距离除以总长度就能得到实际的进度。总长度=layoutHeight-thumbHeight。

    int width = View.MeasureSpec.makeMeasureSpec(0,
        View.MeasureSpec.UNSPECIFIED);
    int height = View.MeasureSpec.makeMeasureSpec(0,
        View.MeasureSpec.UNSPECIFIED);
    measure(width,height);
    //总长度
    if (totalTranY == 0) {
    totalTranY = getMeasuredHeight() - thumb.getMeasuredHeight() - ((LinearLayout.LayoutParams) thumb.getLayoutParams()).topMargin * 2;
    }
  • 设置回调

      //最大进度
      private int maxProgress = 100;
      //进度改变监听
      private ProgressListener mProgressListener = null;
          public interface ProgressListener {
          void onProgress(int progress);
      }
      public void setMaxProgress(int maxProgress) {
          this.maxProgress = maxProgress;
      }
      public void setOnProgressListener(ProgressListener progressListener) {
          this.mProgressListener = progressListener;
      }

优化

  • 正如刚才说的,由于拖拽条尺寸太小,很容易造成用户点不到的情况。在保证UI的不变的情况下增大拖拽面积,也就是将thumb“变大”,这里有两种方法:

    • 设置一张带有透明的图片,让拖拽条看上去很小实际上操作面积相对大。
    • 将thumb做成一个view组,由三个子view组成,分别是上透明区域、可视区域、下透明区域。
      //点击条为了方便用户点击 分为三部分,开始透明点击部分 中间显示部分 结束透明点击部分
      thumbView = new LinearLayout(getContext());
      thumbView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
      thumbView.setOrientation(LinearLayout.VERTICAL);
      //透明点击部分高度
      mThumbHMargin = (int) PublicUtil.getInstance().dpToPx(getContext(), 10);
      //透明点击部分view
      View startView = new View(getContext());
      View endView = new View(getContext());
      LinearLayout.LayoutParams marginParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mThumbHMargin);
      startView.setLayoutParams(marginParams);
      endView.setLayoutParams(marginParams);
      //显示部分
      View thumb = new View(getContext());
      thumb.setBackgroundColor(Color.WHITE);
      int thumbHeight = (int) PublicUtil.getInstance().dpToPx(getContext(), 4);
      LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, thumbHeight);
      layoutParams.topMargin = 5;
      layoutParams.bottomMargin = 5;
      //加入父布局
      thumb.setLayoutParams(layoutParams);
      thumbView.addView(startView);
      thumbView.addView(thumb);
      thumbView.addView(endView);
      addView(thumbView);

  • 在没有任何限制的情况下,thumb可能会被用户拖拽到页面之外,因此需要对可拖拽区域进行限制。

              case MotionEvent.ACTION_MOVE:
                  float changY = event.getY() - lastY;
                  lastY = event.getY();
                  transY += changY;
                  if (transY < 0) {
                      transY = 0;
                  }
                  if (transY > totalTranY) {
                      transY = totalTranY;
                  }
                  moveThumb(transY);
                  if (mProgressListener != null) {
                      int progress = (int) (transY / totalTranY * maxProgress);
                      mProgressListener.onProgress(progress);
                  }
                  break;

    总结

    以上就是主要内容了,写这篇文章的目的是在不利用原生seekbar的情况下用尽可能简单的方式实现高度自定义的功能,在我看来这样要比修改seekbar更为简单和直观,出错率会更低。

    同时也是举一反三,为自定义View提供更多新的思路。

    完整代码

    /**
    * Created by haoran-wang on 2/27/18.
    */
    public class VerticalSeek extends FrameLayout {
    
      private LinearLayout thumbView;
      //最大可滑动距离
      private float totalTranY;
      //最大进度
      private int maxProgress = 100;
      //当前进度
      private int progress = 0;
      //进度改变监听
      private ProgressListener mProgressListener = null;
    
      private int mThumbHMargin;
    
      public VerticalSeek(@NonNull Context context) {
          this(context, null);
      }
    
      public VerticalSeek(@NonNull Context context, @Nullable AttributeSet attrs) {
          this(context, attrs, -1);
      }
    
      public VerticalSeek(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          addThumb();
      }
    
      private void addThumb() {
          //点击条为了方便用户点击 分为三部分,开始透明点击部分 中间显示部分 结束透明点击部分
          thumbView = new LinearLayout(getContext());
          thumbView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
          thumbView.setOrientation(LinearLayout.VERTICAL);
          //透明点击部分高度
          mThumbHMargin = (int) PublicUtil.getInstance().dpToPx(getContext(), 10);
          //透明点击部分view
          View startView = new View(getContext());
          View endView = new View(getContext());
          LinearLayout.LayoutParams marginParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mThumbHMargin);
          startView.setLayoutParams(marginParams);
          endView.setLayoutParams(marginParams);
          //显示部分
          View thumb = new View(getContext());
          thumb.setBackgroundColor(Color.WHITE);
          int thumbHeight = (int) PublicUtil.getInstance().dpToPx(getContext(), 4);
          LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, thumbHeight);
          layoutParams.topMargin = 5;
          layoutParams.bottomMargin = 5;
          thumb.setLayoutParams(layoutParams);
          //拖拽事件
          thumbView.setOnTouchListener(new OnTouchListener() {
              @Override
              public boolean onTouch(View v, MotionEvent event) {
                  if (event.getAction() == MotionEvent.ACTION_DOWN) {
                      isTouch = true;
                  }
                  return false;
              }
          });
          thumbView.addView(startView);
          thumbView.addView(thumb);
          thumbView.addView(endView);
          addView(thumbView);
          //归位
          moveThumb(0);
          //计算总长度
          int width = View.MeasureSpec.makeMeasureSpec(0,
                  View.MeasureSpec.UNSPECIFIED);
          int height = View.MeasureSpec.makeMeasureSpec(0,
                  View.MeasureSpec.UNSPECIFIED);
          measure(width,height);
          if (totalTranY == 0) {
              totalTranY = getMeasuredHeight() - thumb.getMeasuredHeight() - ((LinearLayout.LayoutParams) thumb.getLayoutParams()).topMargin * 2;
          }
      }
    
      private float lastY;
      private float transY = 0;
      private boolean isTouch = false;
    
      @Override
      public boolean onTouchEvent(MotionEvent event) {
          if (!isTouch) {
              return false;
          }
          switch (event.getAction()) {
              case MotionEvent.ACTION_DOWN:
                  lastY = event.getY();
                  break;
              case MotionEvent.ACTION_MOVE:
                  float changY = event.getY() - lastY;
                  lastY = event.getY();
                  transY += changY;
                  if (transY < 0) {
                      transY = 0;
                  }
                  if (transY > totalTranY) {
                      transY = totalTranY;
                  }
                  moveThumb(transY);
                  if (mProgressListener != null) {
                      progress = (int) (transY / totalTranY * maxProgress);
                      mProgressListener.onProgress(progress);
                  }
                  break;
              case MotionEvent.ACTION_UP:
                  isTouch = false;
                  break;
          }
          return true;
      }
    
      private void moveThumb(float transY) {
          thumbView.setTranslationY(transY - mThumbHMargin);
      }
    
      public interface ProgressListener {
          void onProgress(int progress);
      }
    
      public void setMaxProgress(int maxProgress) {
          this.maxProgress = maxProgress;
      }
    
      public void setOnProgressListener(ProgressListener progressListener) {
          this.mProgressListener = progressListener;
      }
    
      public void setProgress(int progress) {
          this.progress = progress;
          transY = totalTranY * (progress / (float) maxProgress);
          moveThumb(transY);
      }
    }
    
        //将需要的背景直接设置为background
       <view.VerticalSeek
           android:id="@+id/vs"
           android:layout_width="25dp"
           android:layout_height="195dp"
           android:layout_gravity="center_horizontal"
           android:background="@drawable/gaiacam_tint"/>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值