某年某月的某一天,在使用链家租房子的时候,偶然看到链家小区详情界面效果,觉得效果很不错。我相信每个攻城狮都希望开发符合自己审美的作品,所以就跟产品bb去了,然后好了,给自己找事做了
刚开始是想在网上找找一些优秀的第三方开源,发现找不到,所以自己操刀了,实现效果如下,UI比较垃圾,嗯,简直想打死自己,这东西都拿的出手。好吧,就是拿出手了,你能拿我怎么滴。
那好,我们一个一个坑来填掉。顺着我踩坑的思路来实现效果。首先,是啥呢,肯定先打点鸡血,选一首节奏不错的歌曲,哈哈哈哈,然后大刀霍霍向横向滚动条。
动手前先分析,该自定义控件可能拥有的公开方法,首先肯定是添加,移除,选中,设置点击监听,滚动监听等,示例,所以我们暂时只定义以下方法
public interface IHorizontalView{
void addItems(List<String> titles);
void selectItem(int index);
void setOnItemClickLisenter(OnItemClickLisenter onItemClickLisenter);
}
先定义拥有自定义View全部公开方法的接口,然后自己定义一个控件,该视图继承LinearLayout,因为只有一个方向嘛,然后实现这个接口
public class HorizontalView extends LinearLayout implements IHorizontalView { ... }
这个控件我们第一眼看过去,就有两个很清晰的功能,一个可以滚动,一个可以点击,那首先要有Item,才可以点击,当Item超过了屏幕才滑动,所以给视图提供添加Item的方法,也可以提供一个SeeSize来决定当前屏幕最多显示的Item数
public void addItems(final List titles) {
//()->是Lambda写法,不需要纠结,为了简化篇幅,简书对代码的适配有点,嗯,去你大爷的
// 由于这个时候无法获取到视图的大小,所以设置布局测量监听,当获取到视图大
//小的时候再结合SeeSize设置Item的宽度
this.getViewTreeObserver().addOnGlobalLayoutListener(()->{
if (isFirstVisible) {
for (int i = 0; i < dataList.size(); i++) {
final TextView textView = (TextView) LayoutInflater.from(context).inflate(R.layout.item_txt, null, false); textView.setText(dataList.get(i));
textView.setWidth(getWidth() / seeSize);
textView.setTag(i);//用来保存当前Item的Position
content.addView(textView);
//获得视图的宽度
viewWidth = viewWidth + getWidth() / seeSize;
}}}}
接下来我们通过 onTouchEvent()这个方法来监听,实现滑动的效果,相信很多人都玩过,通过Down,Move,Up事件,去动态改变的视图的Margin就可以实现视图随着手指移动的效果,这里我们可以自己定义一个最小滑动距离SCROLL_MIN,当滑动的距离超过最小滑动距离的时候,我们认定为滑动,小于最小滑动距离,就认定为是点击
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = event.getRawX();
oldX = ((LayoutParams) this.getLayoutParams()).leftMargin;
break;
case MotionEvent.ACTION_MOVE:
move = x - event.getRawX();
if (move >= SLIDE_MIN) {
setMargin(move, oldX);
}
break;
case MotionEvent.ACTION_UP:
LinearLayout.LayoutParams params = (LayoutParams) content.getLayoutParams();
//边界限定,越界回弹
if (params.leftMargin > 0) params.leftMargin = 0;
if (params.leftMargin + viewWidth - screenWidth <= 0)
params.leftMargin = -(viewWidth - screenWidth);
setLayoutParams(params);
break;
}
return super.onTouchEvent(event);
}
private void setMargin(float move, float lodX) {
LinearLayout.LayoutParams params = (LayoutParams) content.getLayoutParams();
params.leftMargin = (int) (lodX - move);
//到达边界的时候为两边添加阻力, 滑动的距离减少为实际滑动距离缩小4倍的值
if (move < 0 && params.leftMargin >= 0) {
params.leftMargin = (int) ((lodX - move) / 4);
}
if (move > 0 && (params.leftMargin + viewWidth - screenWidth <= 0)) {
params.leftMargin = screenWidth - viewWidth + (params.leftMargin + wiewWidth - screenWidth) / 4;
}
this.setLayoutParams(params);
}
实现点击就更简单啦对不对,直接在addItems()方法中给Item设置点击就好啦,由于点击有交互,所以我们定义一个交互的接口
interface OnItemClickLisenter { void onClick(View view, int position); }
...
textView.setOnClickListener((v)->{
//getTag就是上面设置进去的i
if (Math.abs(move) < SLIDE_MIN){ //注意这个,后面会讲到为什么要有这个判定
selectItem((Integer) v.getTag());//选择点击的Item
if (onItemClickLisenter != null) onItemClickLisenter.onClick(v, (Integer) v.getTag());
}
}});
...
运行的时候会发现,只有textView点击事件被促发,事件全部被点击事件消耗掉了,我们可以在分发事件之前先执行onTouchEvent(),再分发事件,这个时候就可以根据move是否大于滑动最小距离来判定,当前是滑动还是点击(PS:不熟悉事件分发机制,要好好去研究看看哦,很重要)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
this.onTouchEvent(ev);//在这里先处理下你的手势左右滑动事件
return super.dispatchTouchEvent(ev);
}
有部分朋友可能还会遇到一个问题,就是无论你怎么点击,都只有ACTION_DOWN被促发,这个时候只要在初始化的地方添加下面这句代码就可以了,也可以在xml中直接设置
setClickable(true);
接下来就是Item选中的置中效果,只要对边界做一些判断,然后加上动画就可以了
public void selectItem(int index) {
if (index >= dataList.size()) throw new IllegalArgumentException("index out of size");
if (index < 0) throw new IllegalArgumentException("index out of size");
resetAllItemState();
View v = getItemAtIndex(index);
v.setSelected(true);
slidToItem(v);
}
//清除掉选中状态,单选的话也可以通过变量来保存上一个选中position,只清除上个状态
private void resetAllItemState() {
for (int i = 0; i < content.getChildCount(); i++) {
if (content.getChildAt(i) instanceof TextView) //遍历视图中的TextView
content.getChildAt(i).setSelected(false);
} }
private void slidToItem(View v) {//Item置中
LinearLayout.LayoutParams params = (LayoutParams) content.getLayoutParams();
int screenCenterPoint = screenWidth / 2;
if (v.getX() < screenCenterPoint) {
if (params.leftMargin >= 0) return;
if (params.leftMargin < 0) {
doAnimation(300, params.leftMargin, 0);//当点击的Item的位置小于屏幕的中点,左边有部分处于屏 //幕外的将那部分显示出来,如图1
return; } }
if (viewWidth - (v.getX() + v.getWidth() / 2) < screenCenterPoint) {
doAnimation(300, params.leftMargin, -(viewWidth - screenWidth));
return; }
int slideLength = (int) (v.getX() + params.leftMargin + v.getWidth() / 2 - screenCenterPoint); doAnimation(100, params.leftMargin, params.leftMargin - slideLength);
}
private void doAnimation(int duration, int start, int end) {//用属性动画来实现
if (animator != null) animator.cancel();
animator = ValueAnimator.ofInt(start, end);
animator.setDuration(duration);
animator.setInterpolator(new DecelerateInterpolator());
animator.start();
animator.addUpdateListener((animation)->{
LinearLayout.LayoutParams params = (LayoutParams) content.getLayoutParams(); params.leftMargin = (int) animation.getAnimatedValue();
//边界处理
if (params.leftMargin > 0) { params.leftMargin = 0; }
if (params.leftMargin + viewWidth - screenWidth <= 0) {
params.leftMargin = -(viewWidth - screenWidth);
}
setLayoutParams(params);
});
根据滑动的快慢进行惯性滚动,VelocityTracker可以获得当前滑动的速度,在Move的时候获取x方向上的速度,通过不同的速度区间范围进行不同距离的滑动
@Override
public boolean onTouchEvent(MotionEvent event) {
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
...
case MotionEvent.ACTION_MOVE:
velocityTracker.computeCurrentVelocity(1000);
speed = (int) Math.abs(velocityTracker.getXVelocity();
...
break;
...
case MotionEvent.ACTION_UP:
if ( speed > 5000) {
requesDoAnimation((int) move, 300, leftMaigin, 0);
break;
}
if ( speed > 3000) {
requesDoAnimation((int) move, 500, leftMaigin, 400);
break;
}
if ( speed > 500) {
requesDoAnimation((int) move, 300, leftMaigin, 200);
break;
}
...
break;
}
private void requesDoAnimation(int move, int duration, int start, int end) {
//根据正负来判断滑动的方向
if (end == 0) {//如果没有设置end则说明是直接到左右边界,其实有点多余,哈哈哈哈
if (move > 0) end = -(viewWidth - screenWidth);
if (move < 0) end = 0; }
else {
if (move > 0) end = start - end;
if (move < 0) end = start + end; }
doAnimation(duration, start, end);
}
美美的运行吧,然后What,一会儿可以,一会儿不行,什么瞎玩意,又是一个坑,由于在ACTION_UP才处理,所以有时候,当你的手指要抬起的临界点获取到的speed是0,无法惯性滑动,这里提供了我的实现思路,就是用一个List去保存滑动中所有的speed,然后取倒数第二个的speed,应该有更好的的办法,值得思考,到这里就完成啦,其实这个练练手还是很不错的。接下来就是竖向的滚动的啦,直接用ScrollView就可以实现了,然后设置滚动监听,当滚动到对应Title高度的时候就调用我们自定义控件HorizontalView的selectItem()方法就可以了
content.setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY)
{ ... }
});
如果是5.0以下,可能会报找不到这个类的错误,那我们可以换种方法,换成以下方式就能避免
content.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged()
{...}
});
好啦,结束啦,有写的不好的,欢迎喷,多玩玩还是对自己有好处的~
告辞