Android 仿照链家小区详情滚动效果

        某年某月的某一天,在使用链家租房子的时候,偶然看到链家小区详情界面效果,觉得效果很不错。我相信每个攻城狮都希望开发符合自己审美的作品,所以就跟产品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);

}


图1


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()

    {...}

});

好啦,结束啦,有写的不好的,欢迎喷,多玩玩还是对自己有好处的~

告辞

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

推荐阅读更多精彩内容