Android 拖动滑动条、快速滑动,ScrollView和SeekBar结合

本节前言

今天给大家带来的是仿UC、微信端文章浏览的拖动条,可是看似这个拖动条简单,确实一个结合多个知识点的代码,小编也是费了九牛二虎之力才写出来

UC效果

视频可能不是很清楚,这里讲解一下,当我们慢慢滑动的时候,滑动条就是一个简单的ScrollView(此时并不能进行拖动),当我们快速滑动时滑动条变成了一张图片,此时可以拖动图片进行上下浏览文章

原理

这里我先不讲原理,大家可以猜猜他是用什么原理达成这种效果的呢?小编于是问了身边做Android大神的朋友,有说是用Listview(自带拖动效果,不过必须超过屏幕4页),有的人说是用ScrollView,甚至有的人还说是用GridView、FlexboxLayout,小编这里就纳闷了,用GridView、FlexboxLayout怎么写!

其实实现的方法有很多种,小编说一下自己的实践之路

  • 其实看到界面我第一个想到的应该是Scrollview,重写GestureDetector,然后快速滑动的时候然后隐藏Scrollview,然后ImageView显示,失败原因:无法实现拖动效果

  • 然后我又用了Listview+Scrollview,失败原因:1.Listview自带拖动效果,不过必须超过4个屏幕才可以 2.Listview无法自定义拖动效果的样式 3.Scrollview+ListView的滑动条互相冲突,嵌套问题太多

  • 然后最后想到了用ScrollView和SeekBar写出了效果,不过也是很麻烦的

    • 重写SeekBar
    • 重写ScroView
    • ScrollView 的监听和滚动范围
    • 获取屏幕分辨率
    • 可见性和动画
    • 布局
    • 监听ScrollView的滑动状态

来张图解释以下,花了小编足足一小时的时间

仿UC效果图

这里控件比较丑,但是实现效果都是一样的

第一步、重写SeekBar,使其竖直

这里贴一下SeekBar旋转90°的代码

public class VerticalSeekbar extends SeekBar {
    public VerticalSeekbar(Context context) {
        super(context);
    }
    public VerticalSeekbar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    public VerticalSeekbar(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(h, w, oldh, oldw);
    }
    @Override
    public synchronized void setProgress(int progress)  // it is necessary for calling setProgress on click of a button
    {
        super.setProgress(progress);
        onSizeChanged(getWidth(), getHeight(), 0, 0);
    }
    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(heightMeasureSpec, widthMeasureSpec);
        setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
    }
    protected void onDraw(Canvas c) {
        c.rotate(90);//旋转
        c.translate(0, -getWidth());//旋转,这两行不可去掉

        super.onDraw(c);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isEnabled()) {
            return false;
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
                setProgress((int) (getMax() * event.getY() / getHeight()));
                onSizeChanged(getWidth(), getHeight(), 0, 0);
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return true;
    }

}

这段代码理解就行,不过还是建议大家采用封装好的第三方库的SeekBar竖直
第三方库SeekBar

第二步、重写ScrollView

public class ObservableScrollView extends ScrollView {
    public ScrollViewListener scrollViewListener = null;
    public ObservableScrollView(Context context) {
        super(context);
    }
    public ObservableScrollView(Context context, AttributeSet attrs,int defStyle) {
        super(context, attrs, defStyle);
    }
    public ObservableScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
//设置 ScrollViewListener接口
    public interface ScrollViewListener {
        void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy);
    }
//设置接口的setScrollViewListener方法
    public void setScrollViewListener(ScrollViewListener scrollViewListener) {
        this.scrollViewListener = scrollViewListener;
    }

//采取接口回调的方式进行数据传递
    @Override
    public void onScrollChanged(int x, int y, int oldx, int oldy) {
        super.onScrollChanged(x, y, oldx, oldy);
        if (scrollViewListener != null) {
            scrollViewListener.onScrollChanged(this, x, y, oldx, oldy);
        }
    }
}

第三步、ScrollView 的监听和滚动范围

大部分的控件都提供监听者模式的回调,然而不知道为什么 ScrollView 本身没有暴露监听滚动的方法。写个子类开发接口也是可以的,不过笔者这里是直接使用了 Google 官方提供的 NestScrollView。

其中 scrollY 是当前的滚动位置,和可滚动范围的关系图解如下:

可滚动范围的图解

理解了这一层关系之后要做的就很简单了,我们将可滚动范围和 scrollY 映射到 SeekBar 上即可。
滚动绑定

明白了逻辑之后我们只需要将 ScrollView 的滚动映射到 SeekBar,再将 SeekBar 的用户拖动映射回 ScrollView 就行了。以下是笔者封装了的一个辅助类,注释比较详尽:

会重写以下几个方法

public class ScrollBindHelper implements SeekBar.OnSeekBarChangeListener, ObservableScrollView.ScrollViewListener {

    private final SeekBar seekBar;
    private final ObservableScrollView scrollView;
    private final View scrollContent;
    private static ScrollBindHelper helper;
    private static boolean ifii;
    /**
     * 使用静态方法来绑定逻辑,代码可读性更高。
     */
    public static ScrollBindHelper bind(SeekBar seekBar, ObservableScrollView scrollView) {
        //初始化工具类
              //封装好的获取屏幕工具类  进行初始化
            ViewUtil.init(seekBar.getContext().getApplicationContext());

            helper = new ScrollBindHelper(seekBar, scrollView);
            seekBar.setOnSeekBarChangeListener(helper);
            scrollView.setScrollViewListener(helper);
        return helper;
    }

//设置全局属性
    private ScrollBindHelper(SeekBar seekBar, ObservableScrollView scrollView) {
        this.seekBar = seekBar;
        this.scrollView = scrollView;
        //获取scrollview的第一个孩子的高度,在这里第一个孩子就是就是TextView
        this.scrollContent = scrollView.getChildAt(0);
    }

    //用户是否正在拖动SeekBar的标志
    private boolean isUserSeeking;

  //获取TextView的高度
    private int getContentRange() {
        return scrollContent.getHeight();
    }
    //获取滚动范围
    private int getScrollRange() {
         //换句话说就是TextView的高度 -  Scrollview的高度
        return scrollContent.getHeight() - scrollView.getHeight();
    }

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            showScroll();

        if (!isUserSeeking) {
            handler.reset();
        }

        //不是用户操作的时候不触发
        if (!fromUser) {
            return;
        }

        scrollView.scrollTo(0, progress * getScrollRange() / 100);
    }

      //SeekBar的拖动事件
    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        isUserSeeking = true;
        ifii=false;
        handler.clearAll();
    }

    //SeekBar的停止拖动事件
    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        isUserSeeking = false;
        handler.reset();
    }
    /*动画*/
    public static final long DEFAULT_TIME_OUT = 1000L;

    @Override
    public void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy) {
        //用户触控时不触发
        if (isUserSeeking) {
            return;
        } else if (getContentRange() < ViewUtil.getScreenHeightPx() * 3) {//宽度小于三个屏幕不做处理
            return;
        }

        int range = getScrollRange();
        seekBar.setProgress(range != 0 ? y * 100 / range : 0);
    }

    private static class VisibleHandler extends LastMsgHandler {

        private ScrollBindHelper helper;

        public VisibleHandler(ScrollBindHelper helper) {
            this.helper = helper;
        }

        public void reset() {
            sendMsgDelayed(DEFAULT_TIME_OUT);
        }

        @Override
        protected void handleLastMessage(Message msg) {
            helper.hideScroll();
        }
    }
    private VisibleHandler handler = new VisibleHandler(this);

//隐藏SeekBar
    private void hideScroll() {
        seekBar.setVisibility(View.GONE);
    }
//显示SeekBar
    private void showScroll() {
        seekBar.setVisibility(View.VISIBLE);
    }}

第四步获取屏幕分辨率

用的网上封装好的屏幕相关类,在原基础小编添加了注释,写的很清楚,如果看不懂的可以去复习学习获取屏幕的知识点

public final class ViewUtil {

    private ViewUtil() {
    }
    /*视图参数*/
    private static float density;
    private static float scaledDensity;
    private static int widthPixels;
    private static int heightPixels;

    private static boolean isInit = false;

    private static void confirmInit() {
        if (!isInit) {
            throw new IllegalStateException("ViewUtil还未初始化");
        }
    }

    public static void init(Context context) {
        if (isInit) {
            return;
        }
             // Displaymetrics 是取得手机屏幕大小的关键类
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
          // 显示器的逻辑密度,Density Independent Pixel(如3.0)
        density = displayMetrics.density;
              //缩放密度(scaledDensity和density数值是一样的)
        scaledDensity = displayMetrics.scaledDensity;
              
                //屏幕的像素宽度
        widthPixels = displayMetrics.widthPixels;
                //屏幕的像素高度
        heightPixels = displayMetrics.heightPixels;
        isInit = true;
    }

    public static float getDisplayMetricsDensity() {
        confirmInit();
        return density;
    }

    public static float getDisplayMetricsScaledDensity() {
        confirmInit();
        return scaledDensity;
    }

    public static int getScreenWidthPx() {
        confirmInit();
        return widthPixels;
    }

    public static int getScreenHeightPx() {
        confirmInit();
        return heightPixels;
    }

    /* 单位转换 */

    public static int dpToPx(float dpValue) {
        confirmInit();
        return (int) (dpValue * getDisplayMetricsDensity() + 0.5F);
    }

    public static int pxToDp(float pxValue) {
        confirmInit();
        return (int) (pxValue / getDisplayMetricsDensity() + 0.5F);
    }

    public static int pxToSp(float pxValue) {
        confirmInit();
        return (int) (pxValue / getDisplayMetricsScaledDensity() + 0.5f);
    }

    public static int spToPx(float spValue) {
        confirmInit();
        return (int) (spValue * getDisplayMetricsScaledDensity() + 0.5f);
    }
}

可见性和动画

我们知道滚动视图的滑块并不是一直存在的,它有着如下的行为:

  • 默认不可见

  • 内容视图高度太小,比如小于三个屏幕高度时不出现滑块

  • 出现之后界面滚动或者滑块被用户触控时滑块不会消失

  • 停止操作若干毫秒后消失

可见性的切换我们只要切换滑块控件的 Visible 属性即可而滚动和触控都在我们的回调之中,最后停止操作若干秒后消失可以直接使用一个 handler 搞定。我们创建一个只响应最后一次操作的 Handler 基类。

//只响应最后一次操作的基类
public abstract class LastMsgHandler extends Handler {

    //标记是第几次count
    private int count = 0;

    /**
     * 增加Count数。
     */
    public synchronized final void increaseCount() {
        count++;
    }

    //直接发送消息
    public final void sendMsg() {
        sendMsgDelayed(0);
    }

    //增加count数后发送延时消息
    //如果延时小于或者等于0则直接发送。
    public final void sendMsgDelayed(long delay) {
        increaseCount();
        if (delay <= 0) {
            sendEmptyMessage(0);
        } else {
            sendEmptyMessageDelayed(0, delay);
        }
    }

    //清空所有count和消息
    public synchronized final void clearAll() {
        count = 0;
        removeCallbacksAndMessages(null);
    }

    @Override
    public synchronized final void handleMessage(Message msg) {
        super.handleMessage(msg);
        count--;

        //确保count数不会异常
        if (count < 0) {
            throw new IllegalStateException("count数异常");
        }

        //当count为0时说明是最后一次请求
        if (count == 0) {
            handleLastMessage(msg);
        }
    }

    //响应最后一次请求
    protected abstract void handleLastMessage(Message msg);
}

布局

接下来就进入收尾阶段了

布局就一个

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    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"
    tools:context=".activity.MainActivity"
    android:id="@+id/fl">

    <core.scroll.activity.ObservableScrollView
        android:id="@+id/nestedScrollView_frag_edit"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/textView_main_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@null"
            android:text="@string/content"
            android:padding="8dp"
            android:singleLine="false"/>

    </core.scroll.activity.ObservableScrollView>

    <com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBarWrapper
        android:layout_width="24dp"
        android:layout_height="match_parent"
        android:layout_gravity="end"

        >

        <com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBar
            android:id="@+id/seekBar_main_scrollThumb"
            android:thumb="@drawable/scrollbar_drag"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="gone"
            app:seekBarRotation="CW90"/> <!-- Rotation: CW90 or CW270 -->
    </com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBarWrapper>

</FrameLayout>

MainActivity

public class MainActivity extends AppCompatActivity {

    private GestureDetector gestureDetector;
    private ObservableScrollView scrollView;
    private SeekBar seekBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        seekBar = (SeekBar) findViewById(R.id.seekBar_main_scrollThumb);
        scrollView = (ObservableScrollView) findViewById(R.id.nestedScrollView_frag_edit);
                
              ScrollBindHelper.bind(seekBar, scrollView);
}
}

实现效果是这样的

其实还没有达到我们想要的结果,如果想要达到uc那种效果的话,其实也不难,重写GestureDetector的onFling,当Math.abs(motionEvent.getY()-motionEvent1.getY()大于一定距离才会出现SeekBar,

今天的教程就到这里,喜欢的就点个赞吧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容