本节前言
今天给大家带来的是仿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);
}
}
实现效果是这样的