引言
在与ViewPager完美结合的顺滑引导条系列之需求分析这篇文章里已经了解了基本的需求,分析了一些交互行为。布局方面,我首先想到的是用RecyclerView完成索引,目的是让索引项可以复用减少内存开销,然后画一个View做光标。
滑动逻辑,是通过ViewPager.OnPageChangeListener中onPageScrolled回调的第二个参数positionOffset获得ViewPager滑动的比例,可以通过此参数计算光标和索引需要滑动的距离
正文
为了保证通用性将引导条封装成一个单独的控件GuideRecyclerView,布局如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="44dp">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="44dp" />
<View
android:id="@+id/lineView"
android:layout_width="10dp"
android:layout_height="3dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="7dp"
android:background="#444444" />
</RelativeLayout>
使用时只需要三行代码
guideRecyclerView = findViewById(R.id.guiderecyclerview);
guideRecyclerView.setAdapter(recyclerViewAdapter);
viewPager.addOnPageChangeListener(guideRecyclerView.getOnPageChangeListener());
这里的Adapter是RecyclerView的,数据也采用最简单的List<String>。OnPageChangeListener则是在GuideRecyclerView内部实现方便复用。
GuideRecyclerView
重点代码分析
public ViewPager.OnPageChangeListener getOnPageChangeListener() {
return new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
Log.i("onPageScrolled", "position-->" + position + " positionOffset-->" + positionOffset + " positionOffsetPixels-->" + positionOffsetPixels);
setScroll(position, positionOffset);
}
};
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (lineViewMarginLeft == 0) {//第一次布局游标
layoutLineView(0);
}
}
private void setScroll(int position, float offset) {
Log.i("setScroll", "position-->" + position + " offset-->" + offset);
if (!whetherOrNotToMove(position, offset)) {
return;
}
if (!positiveDirection) {//根据方向设置偏移
offset = offset - 1;
}
int x = calculateMoveLength(position, offset);
move(x);
}
private boolean whetherOrNotToMove(int position, float offset) {
if (position > currentPosition) {//第一次正向滑动触发
resetState(position);
layoutLineView(currentPosition);
positiveDirection = true;
return false;
}
if (position < currentPosition) {//第一次反向滑动触发
resetState(position);
layoutLineView(currentPosition + 1);
positiveDirection = false;
return false;
}
if (position == 0 && offset == 0) {//滑到第一个 初始状态
resetState(position);
layoutLineView(0);
positiveDirection = true;
return false;
}
return true;
}
private void resetState(int position) {//重置状态
currentPosition = position;
whichOne = 0;
lastx = 0;
}
private void layoutLineView(int currentPosition) {//将游标布局到指定位置
View item = recyclerView.getLayoutManager().findViewByPosition(currentPosition);
lineViewMarginLeft = item.getLeft() + (item.getWidth() - lineView.getWidth()) / 2;
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) lineView.getLayoutParams();
params.leftMargin = lineViewMarginLeft;
lineView.setLayoutParams(params);
}
private int calculateMoveLength(int position, float offset) {
// recyclerView.getChildCount() 只有当前显示的数目
// recyclerView.getChildAt() 只能获取当前显示的view
int temp = position;
if (temp >= recyclerView.getAdapter().getItemCount()) {
temp = recyclerView.getAdapter().getItemCount() - 1;
}
View currentView = recyclerView.getLayoutManager().findViewByPosition(temp);//当前所指view
int lastPosition = position + 1;
if (lastPosition >= recyclerView.getAdapter().getItemCount()) {
lastPosition = recyclerView.getAdapter().getItemCount() - 1;
}
View nextView = recyclerView.getLayoutManager().findViewByPosition(lastPosition);//将要滑动到的view
Log.i("viewnull", temp + " currentView-->" + currentView + "----------" + lastPosition + " nextView-->" + nextView);
int width = (currentView.getWidth() + nextView.getWidth()) / 2;//计算两view之间的总的滑动距离
return Math.round(width * offset);//此次滑动的距离
}
private void move(int x) {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) lineView.getLayoutParams();
if (whichOne == 0) {//判断 一次完整的滑动 是否结束 一次滑动结束后 whichOne=0
if (positiveDirection) {
if (params.leftMargin > 540 && recyclerView.canScrollHorizontally(1)) {//游标在右半边且recyclerView可以右滑 则滑动recyclerView
whichOne = whichOne | 1;
} else {
whichOne = whichOne | 2;
}
} else {
if (params.leftMargin < 540 && recyclerView.canScrollHorizontally(-1)) {//游标在左半边且recyclerView可以左滑 则滑动recyclerView
whichOne = whichOne | 1;
} else {
whichOne = whichOne | 2;
}
}
}
if (whichOne == 1) {
recyclerView.scrollBy(x - lastx, 0);//因为scrollBy的滑动是累加的,所以必须减去上次滑动的距离,实际移动的距离为差值 exp:第一次滑动1 第二次滑动2,第二次滑动3,scrollBy会滑动6,但实际我只想滑动3
lastx = x;
} else {
params.leftMargin = lineViewMarginLeft + x;
lineView.setLayoutParams(params);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {//viewpager还在滑动的时候recyclerView不许滑动 算了直接不让滑动否则会造成空指针
return true;
}
初始化
控件首次显示时会调用onLayout和onPageScrolled
最初显示时游标并不知道自己的位置所以要在onLayout中调用layoutLineView(0)一次,让游标初始化起始位置
onPageScrolled是ViewPager初次显示时的回调,此时默认参数全部为0
滑动
ViewPager正向滑动时position不变positionOffset逐渐变大
ViewPager反向滑动时position即刻减一positionOffset逐渐减小
positionOffset范围:[0,1)
从代码可以看到当ViewPager滑动的时候会执行setScroll方法,其内部主要逻辑如下:
whetherOrNotToMove:返回是否需要滑动,内部做了如下处理:重置状态、布局游标、判断滑动方向
resetState:当一次完整的滑动结束后状态重置
layoutLineView:当一次完整的滑动结束后摆放游标位置
由于反向滑动positionOffset是逐渐减小的,于是需要转
if (!positiveDirection) {//根据方向设置偏移
offset = offset - 1;
}
calculateMoveLength:计算滑动的距离
move:移动
whetherOrNotToMove 判断是否滑动
三个if代表了三种不同情况下滑动状态的终结
情景分析:
如果最初在0位置,手势左滑,此时currentPosition=0,参数position=0,offset逐渐增加,此时返回true,表示滑动开始,只有在position== currentPosition时才会滑动
第一个if:当滑动到1位置时,此时参数position=1,offset可忽略(因为不固定,并不一定返回0),这时是第1项选中的状态,完成了一次滑动,因此需要重置状态将游标摆在第1项下,记录滑动的方向,并返回false,表示一次滑动结束
第二个if:反向滑动时position立即减1,与当前currentPosition不同,做滑动前的状态重置,将游标放在当前的项下而不是目标项下记录滑动的方向,并返回false
第三个if:这时为了提防onPageScrolled首次回调使用,与onLayout同理resetState重置状态
将传入的position赋值当前位置currentPosition。
将上一次滑动对象是谁的状态清除。
lastx代码中有注释需要注意layoutLineView将游标摆放特定位置
获取显示项View距父控件的x坐标,并将游标居中于显示项,算出距离设置MargincalculateMoveLength计算移动距离
得到当前项和目标项View,计算两View中心点间的距离乘以偏移量就得出了移动距离。
这里注意RecyclerView中getChildCount和getChildAt两个方法,它是获取当前显示的ChildCount和当前显示的ChildAt,注意使用move移动
这里可移动的有光标和索引因此需要记录此次滑动的是谁,如果上次滑动没有结束继续上次滑动。
最终效果
缺陷
- 索引滑动时游标不动
- 一次完整的滑动必须是索引或游标,不能共存
- 频繁不规则地滑动ViewPager或引导条时,会引起NullPointerException,因为RecyclerView找不到内部的View(所以才重写了dispatchTouchEvent)
为了解决上述问题见与ViewPager完美结合的顺滑引导条系列之二