当前位置: 七九推 > 移动技术>移动开发>Android > Android自定义ViewGroup实现右滑进入详情

Android自定义ViewGroup实现右滑进入详情

2023年01月15日 Android 我要评论
前言在之前的 viewgroup 的事件相关一文中,我们详细的讲解了一些常见的 viewgroup 需要处理的事件与运动的方式。我们了解了如何处理拦截事件,如何滚动,如何处理子 view 的协调运动等

前言

在之前的 viewgroup 的事件相关一文中,我们详细的讲解了一些常见的 viewgroup 需要处理的事件与运动的方式。

我们了解了如何处理拦截事件,如何滚动,如何处理子 view 的协调运动等。

再复杂一点,我们可以组合在一起使用。例如在拦截事件之后滚动,或者在滚动到一个阈值之后拦截事件。

今天我们一起再巩固一下相关的知识点,以比较常见的一个应用场景,右滑进入详情的场景为例子。

这个例子中又分几种常见的类型,以几个头部app为例的话:

1. 一种是类似抖音列表的的右滑直接详情:

2. 一种是类似闲鱼这种右滑提示再进入详情

3. 另一种是类似豆瓣这种列表滑动进入详情

接下来我们就一起复习一下,看看都能怎么实现。

话不多说,let's go

一、抖音直接右滑进入详情

其实抖音的这种效果实现的方式有很多,比如 viewpager 是最简单的 ,但是抖音的首页本身就是一个垂直的 viewpager(rv) ,内部的 item 再用横向的 viewpager 做内容与详情的切换?要这么做吗?能不能这么做?

能当然能,但是呢,没必要。

一般这种简单的效果,我们一般使用自定义 viewgroup 即可实现轻量的效果,不需要整那么“笨重” 。

那么自定义 viewgroup 如何实现这种效果呢? 总归是记录点击坐标,记录移动坐标,然后对对应的子view做移动,例如 translationx 、scroller 都可以完成类似的逻辑,在放开的时候滚动回指定的位置即可。

确实,这样是标准的做法,也不是不行,但是我们这个效果并不涉及到事件的拦截与一些处理,其实我们可以使用更简单的方式 viewdraghelper 来实现,它内部集成了移动事件的判断与移动的逻辑封装,还能让子view协调运动,也是特别适合这个场景。

如何使用呢?代码如下:

public class douyinview5 extends framelayout {

    private view contentview;
    private view detailview;
    private int contentwidth;
    private int contentheight;
    private int detailwidth;
    private int detailheight;
    private viewdraghelper viewdraghelper;
    private float downx;
    private float downy;

    public douyinview5(context context) {
        super(context);
        init();
    }

    public douyinview5(context context, attributeset attrs) {
        super(context, attrs);
        init();
    }

    public douyinview5(context context, attributeset attrs, int defstyleattr) {
        super(context, attrs, defstyleattr);
        init();
    }

    private void init() {
        viewdraghelper = viewdraghelper.create(this, callback);
    }

    @override
    public boolean onintercepttouchevent(motionevent ev) {
        return viewdraghelper.shouldintercepttouchevent(ev);
    }

    @override
    public boolean ontouchevent(motionevent event) {

        switch (event.getaction()) {
            case motionevent.action_down:
                downx = event.getx();
                downy = event.gety();
                break;

            case motionevent.action_move:
                float movex = event.getx();
                float movey = event.gety();
                float dx = movex - downx;
                float dy = movey - downy;

                if (math.abs(dx) > math.abs(dy)) {
                    requestdisallowintercepttouchevent(true);
                }

                downx = movex;
                downy = movey;
                break;
            case motionevent.action_up:
                break;
        }

        viewdraghelper.processtouchevent(event);

        return true;
    }

    //完成初始化,获取控件
    @override
    protected void onfinishinflate() {
        super.onfinishinflate();
        contentview = getchildat(0);
        detailview = getchildat(1);
    }

    /**
     * 完成测量时调用,获取高度,宽度
     */
    @override
    protected void onsizechanged(int w, int h, int oldw, int oldh) {
        super.onsizechanged(w, h, oldw, oldh);
        contentwidth = contentview.getmeasuredwidth();
        contentheight = contentview.getmeasuredheight();
        detailwidth = detailview.getmeasuredwidth();
        detailheight = detailview.getmeasuredheight();
    }

    /**
     * 调用方法完成位置的布局
     */
    @override
    protected void onlayout(boolean changed, int left, int top, int right, int bottom) {
        contentview.layout(0, 0, contentwidth, contentheight);
        detailview.layout(contentwidth, 0, contentwidth + detailwidth, detailheight);
    }

    private viewdraghelper.callback callback = new viewdraghelper.callback() {

        @override
        public boolean trycaptureview(view child, int pointerid) {
            return child == contentview || child == detailview;
        }

        @override
        public int getviewhorizontaldragrange(view child) {
            return detailwidth;
        }

        @override
        public int clampviewpositionhorizontal(view child, int left, int dx) {
            //边界的限制
            if (child == contentview) {
                if (left > 0) left = 0;
                if (left < -detailwidth) left = -detailwidth;
            } else if (child == detailview) {
                if (left > contentwidth) left = contentwidth;
                if (left < contentwidth - detailwidth) left = contentwidth - detailwidth;
            }
            return left;
        }

        @override
        public void onviewpositionchanged(view changedview, int left, int top, int dx, int dy) {
            super.onviewpositionchanged(changedview, left, top, dx, dy);
            //做内容布局移动的时候,详情布局跟着同样的移动
            if (changedview == contentview) {
                detailview.layout(detailview.getleft() + dx, detailview.gettop() + dy,
                        detailview.getright() + dx, detailview.getbottom() + dy);
            } else if (changedview == detailview) {
                //当详情布局移动的时候,内容布局做同样的移动
                contentview.layout(contentview.getleft() + dx, contentview.gettop() + dy,
                        contentview.getright() + dx, contentview.getbottom() + dy);
            }

        }

        @override
        public void onviewreleased(view releasedchild, float xvel, float yvel) {
            super.onviewreleased(releasedchild, xvel, yvel);
            //松开之后,只要移动超过一半就可以打开或者关闭
            if (contentview.getleft() < -detailwidth / 2) {
                open();
            } else {
                close();
            }
        }
    };

    public void open() {
        viewdraghelper.smoothslideviewto(contentview, -detailwidth, 0);
        viewcompat.postinvalidateonanimation(this);
    }


    public void close() {
        viewdraghelper.smoothslideviewto(contentview, 0, 0);
        viewcompat.postinvalidateonanimation(this);
    }

    @override
    public void computescroll() {
        if (viewdraghelper.continuesettling(true)) {
            viewcompat.postinvalidateonanimation(this);
        }
    }

}

除去测量布局的代码(继承了framlayout,不需要我们自己手动测量了),再除去 viewdraghelper 的模板代码。核心代码就那么10多行。

这样即可实现简单的效果了:

是不是很简单!

而有些同学可能会说 viewdraghelper 好麻烦,我还需要在移动的时候处理事件呢,也不方便用 viewdraghelper ,能不能使用基本的方式来实现呢?

二、闲鱼右滑进入详情

确实,如果内部有多个view ,还涉及到一些事件的拦截与处理,我们可以使用基本的 motionevent 来判断。

这里以闲鱼的右滑进入详情为例子,我们需要在滑动的时候记录移动值,然后让右侧的滑块绘制对应的贝塞尔背景,并且这个 textview 还是竖直排列文本的,所以我们需要先自定义一个这个特殊的 textview 。

完整的代码如下:

/**
 * 右侧的查看滑动更多,竖版排列文本效果,并绘制贝塞尔曲线背景
 */
public class showmoretextview extends appcompattextview {

    // 默认文本
    private charsequence mdefaulttext = "更多";

    //默认使用文本画笔
    protected textpaint mtextpaint;
    //每个文字的间距
    private int mcharspacing;

    // 贝塞尔阴影画笔
    private paint mshadowpaint;
    // 贝塞尔的路径
    private path mshadowpath;
    //贝塞尔曲线的控制点-变量动态控制
    private float mshadowoffset = 0;
    //默认的间距
    private int mnormalspaceing;

    public showmoretextview(context context) {
        this(context, null);
    }

    public showmoretextview(context context, @nullable attributeset attrs) {
        this(context, attrs, 0);
    }

    public showmoretextview(context context, @nullable attributeset attrs, int defstyleattr) {
        super(context, attrs, defstyleattr);

        //画笔的一些配置
        mtextpaint = new textpaint(paint.anti_alias_flag);
        mtextpaint.setantialias(true);

        //默认间距
        mcharspacing = commutils.dip2px(4);
        mnormalspaceing = commutils.dip2px(8);

        //画笔赋值
        mshadowpaint = new paint();
        mshadowpaint.setcolor(color.parsecolor("#4fcccccc"));
        mshadowpaint.setantialias(true);
        mshadowpaint.setstyle(paint.style.fill);
        mshadowpaint.setstrokewidth(1);

        mshadowpath = new path();

    }

    @override
    protected void ondraw(canvas canvas) {
        super.ondraw(canvas);

        mtextpaint.settextsize(gettextsize());
        mtextpaint.setcolor(getcurrenttextcolor());
        mtextpaint.settypeface(gettypeface());

        //竖版文本的绘制
        charsequence text = mdefaulttext;
        if (text != null && !text.tostring().trim().equals("")) {
            rect bounds = new rect();
            mtextpaint.gettextbounds(text.tostring(), 0, text.length(), bounds);

            float startx = getlayout().getlineleft(0) + getpaddingleft();

            //处理drawleft的间距
            if (getcompounddrawables()[0] != null) {
                rect drawrect = getcompounddrawables()[0].getbounds();
                startx += (drawrect.right - drawrect.left);
            }

            startx += getcompounddrawablepadding();

            float starty = getbaseline();

            //不处理bounds会导致间距异常
            int cheight = (bounds.bottom - bounds.top + mcharspacing);

            // 居中水平对齐
            starty -= (text.length() - 1) * cheight / 2;

            for (int i = 0; i < text.length(); i++) {
                string c = string.valueof(text.charat(i));
                canvas.drawtext(c, startx, starty + i * cheight, mtextpaint);
            }

        }

        // 动态的绘制贝塞尔的背景
        mshadowpath.reset();
        mshadowpath.moveto(getwidth(), 0);
        mshadowpath.quadto(mshadowoffset, getheight() / 2, getwidth(), getheight());
        canvas.drawpath(mshadowpath, mshadowpaint);

    }

    @override
    public void settext(charsequence text, buffertype type) {
        mdefaulttext = text;
        super.settext("", type);
    }

    public void setverticaltext(charsequence text) {
        if (textutils.isempty(text)) return;
        mdefaulttext = text;
        invalidate();
    }


    /**
     * 暴露的方法,控制贝塞尔曲线的控制点
     */
    public void setshadowoffset(float offset, float maxoffset) {

        this.mshadowoffset = offset;
        float dis = maxoffset / 2 - mnormalspaceing;
        if (mshadowoffset >= dis) {
            mshadowoffset = dis;
        } else {
            mshadowoffset = dis + (offset - dis) / 2;
        }
        invalidate();
    }
}

主要是基于变量 mshadowoffset 来绘制贝塞尔背景,然后就是其中绘制文本的一些控制了。

而我们主要的容器则是继承自 viewgroup , 之前是继承了framelayout ,不需要我们测量,现在测量布局都需要我们自己来了。

在我们之前的文章中,我们都已经反复的复习过了,这里就快速跳过这些非重点代码:

  //完成初始化,获取控件
    @override
    protected void onfinishinflate() {
        super.onfinishinflate();
        mcontentview = getchildat(0);
        mmoretextview = (showmoretextview) getchildat(1);
    }

    @override
    protected void onsizechanged(int w, int h, int oldw, int oldh) {
        super.onsizechanged(w, h, oldw, oldh);
        contentwidth = mcontentview.getmeasuredwidth();
        contentheight = mcontentview.getmeasuredheight();
        showmoreviewwidth = mmoretextview.getmeasuredwidth();
        showmoreviewheight = mmoretextview.getmeasuredheight();

        //右侧布局的偏移量
        moffsetwidth = -showmoreviewwidth;

    }

    @override
    protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {

        //测量真正的容器的布局
        measurechild(mcontentview, widthmeasurespec, heightmeasurespec);

        //测量showmore布局
        measurechild(mmoretextview, widthmeasurespec, heightmeasurespec);

        this.setmeasureddimension(mcontentview.getmeasuredwidth(), mcontentview.getmeasuredheight());
    }

    @override
    protected void onlayout(boolean changed, int left, int top, int right, int bottom) {

        mcontentview.layout(0, 0, contentwidth, contentheight);

        mmoretextview.layout(contentwidth, contentheight / 2 - showmoreviewheight / 2,
                contentwidth + showmoreviewwidth, contentheight / 2 - showmoreviewheight / 2 + showmoreviewheight);
    }

接下来就是记录坐标点,移动的坐标点,以及取消事件的动画,基本上可以认为是一套模板代码,可以套用到类似的效果上。

  @override
    public boolean ontouchevent(motionevent ev) {

        switch (ev.getaction()) {
            case motionevent.action_down:
                mhintleftmargin = 0;
                mlastx = ev.getrawx();
                mlasty = ev.getrawy();
                break;
            case motionevent.action_move:

                // 释放动画
                if (mreleasedanim != null && mreleasedanim.isrunning()) {
                    break;
                }

                mdeltax = (ev.getrawx() - mlastx);
                mdeltay = ev.getrawy() - mlasty;

                mlastx = ev.getrawx();
                mlasty = ev.getrawy();

                mdeltax = mdeltax * ratio;

                //滑动的赋值
                if (mdeltax > 0) {
                    // 右滑
                    sethinttexttranslationx(mdeltax);

                } else if (mdeltax < 0) {
                    // 左滑
                    sethinttexttranslationx(mdeltax);
                }
                break;

            case motionevent.action_cancel:
            case motionevent.action_up:

                //拦截事件-父布局滚
                getparent().requestdisallowintercepttouchevent(false);

                // 释放动画
                if (mreleasedanim != null && mreleasedanim.isrunning()) {
                    break;
                }

                //如果达到指定位置了才算释放
                if (moffsetwidth != 0 && mhintleftmargin <= moffsetwidth && mlistener != null) {
                    mlistener.onrelease();
                }

                //默认的回去动画
                mreleasedanim = valueanimator.offloat(1.0f, 0);
                mreleasedanim.setduration(300);
                mreleasedanim.addupdatelistener(new valueanimator.animatorupdatelistener() {
                    @override
                    public void onanimationupdate(valueanimator animation) {
                        float value = (float) animation.getanimatedvalue();

                        mmoretextview.settranslationx(value * mmoretextview.gettranslationx());
                    }
                });
                mreleasedanim.start();

                break;

        }

        return true;
    }

    /**
     * 设置showmore布局的偏移量,并且设置内部重绘贝塞尔曲线的控制点变量
     */
    private void sethinttexttranslationx(float deltax) {

        float offsetx = 0;
        if (mmoretextview != null) {
            mhintleftmargin += deltax;
            if (mhintleftmargin <= moffsetwidth) {
                offsetx = moffsetwidth;
                mmoretextview.setverticaltext(release_more);
            } else {
                offsetx = mhintleftmargin;
                mmoretextview.setverticaltext(scroll_more);
            }
            mmoretextview.setshadowoffset(offsetx, moffsetwidth);
            mmoretextview.settranslationx(offsetx);

            yylogutils.w("settranslationx:" + offsetx);
        }

    }

核心的逻辑是拿到了移动变量之后设置右侧的 showmoreview 的 settranslationx 与它内部的 mshadowoffset 变量,从而达到绘制贝塞尔背景的效果。

这里我们的移动是使用的 settranslationx ,取消事件使用的是属性动画的方式,当然了使用其他方式例如,我们移动都交给 scroller 来完成也是可以的。

效果:

同样的效果,其实我们甚至可以直接使用 viewdraghelper 来实现更为简单,怎么说了,为了下面的例子扩展,我们先选择使用 motionevent + settranslationx 的方式实现,如果有兴趣,大家可以自行使用不同的方式来实现,接下来就是看如何在滚动的列表中加入右滑进入详情的逻辑了。

三、列表的右滑进入详情

如果说之前的效果都可以用 viewdraghelper 来简化完成,那么这种带列表的滚动我们还是最好自己来处理事件与移动与拦截。

对比来说,唯一麻烦的就是我们需要在左侧的rv滚动的时候去及时的处理拦截事件。移动的也好处理,我们可以直接设置左侧rv的 translationx 移动 和 右侧showmoreview 的 translationx 移动。这样就能达到移动的效果。

在我们之前的例子基础上实现,还是基于 settranslationx 来移动,并且使用属性动画来做释放的逻辑,我们再之前的代码上修改一番。

首先我们的布局应该是如下的:

showmoretextview 我们已经很了解了,他就两个功能,第一个就是垂直的文本排列,第二个就是通过一个入参变量控制贝塞尔曲线的控制点。为了简单我就直接使用上一个效果的view了。

此效果的重点就是如何自定义 viewgroup ,处理对应的排列,移动,与事件拦截。

首先一个 viewgroup 需要先完成的就是测量与布局:

public class viewgroup5 extends viewgroup {

    private recyclerview mhorizontalrecyclerview;
    private showmoretextview mmoretextview;

    private int rvcontentwidth;
    private int rvcontentheight;
    private int showmoreviewwidth;
    private int showmoreviewheight;


   //展示之后获取宽高信息
    @override
    protected void onsizechanged(int w, int h, int oldw, int oldh) {
        super.onsizechanged(w, h, oldw, oldh);

        rvcontentwidth = mhorizontalrecyclerview.getmeasuredwidth();
        rvcontentheight = mhorizontalrecyclerview.getmeasuredheight();
        showmoreviewwidth = mmoretextview.getmeasuredwidth();
        showmoreviewheight = rvcontentheight;

        //右侧布局的偏移量
        moffsetwidth = -showmoreviewwidth;
    }

    //完成初始化,获取控件
    @override
    protected void onfinishinflate() {
        super.onfinishinflate();
        mhorizontalrecyclerview = (recyclerview) getchildat(0);
        mmoretextview = (showmoretextview) getchildat(1);
    }

    @override
    protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {

        //rv测量 - 默认的测量不改动
        measurechild(mhorizontalrecyclerview, widthmeasurespec, heightmeasurespec);
        int width = mhorizontalrecyclerview.getmeasuredwidth();
        int height = mhorizontalrecyclerview.getmeasuredheight();

        //右侧showmore的测量 - 自行改动高度测量
        final layoutparams lp = mmoretextview.getlayoutparams();
        mmoretextview.measure(
                getchildmeasurespec(widthmeasurespec, mmoretextview.getpaddingleft() + mmoretextview.getpaddingright(), lp.width),
                getchildmeasurespec(measurespec.exactly, mmoretextview.getpaddingtop() + mmoretextview.getpaddingbottom(), height)
        );

        //指定viewgroup的测量 - 父布局的测量就是rv的宽高
        this.setmeasureddimension(width, height);
    }

    @override
    protected void onlayout(boolean changed, int left, int top, int right, int bottom) {
        mhorizontalrecyclerview.layout(0, 0, rvcontentwidth, rvcontentheight);
        mmoretextview.layout(mhorizontalrecyclerview.getright(), 0, mhorizontalrecyclerview.getright() + showmoreviewwidth, showmoreviewheight);
    }

}

在之前的文章中,我们反复的复习过测量与布局,这里就一笔带过,接下来就是事件的处理与移动。并且在 viewgroup 分发事件,判断是否拦截事件。

  • 当滑动到最左侧的时候我们可以继续滑动,给内部的两个布局设置 settranslationx 从而达到移动的效果。
  • 当滑动到最右侧的时候,我们同样可以继续滑动,但是内部的方法就可以判断设置 setshadowoffset 去设置贝塞尔曲线的显示。
  • 当滑动到中间的时候,我们不拦截事件,我们把事件给rv,所以当前滚动的是rv 控件。

具体实现如下:

    @override
    public boolean dispatchtouchevent(motionevent ev) {
        if (mhorizontalrecyclerview == null) {
            return super.dispatchtouchevent(ev);
        }
        switch (ev.getaction()) {
            case motionevent.action_down:
                mhintleftmargin = 0;
                mmoveindex = 0;
                mlastx = ev.getrawx();
                mlasty = ev.getrawy();
                break;
            case motionevent.action_move:
                // 释放动画
                if (mreleasedanim != null && mreleasedanim.isrunning()) {
                    break;
                }
                float mdeltax = (ev.getrawx() - mlastx);
                float mdeltay = ev.getrawy() - mlasty;

                mlastx = ev.getrawx();
                mlasty = ev.getrawy();
                mdeltax = mdeltax * ratio;

                //滑动的赋值
                if (mdeltax > 0) {
                    // 右滑并判断是否滑动到边缘
                    if (!mhorizontalrecyclerview.canscrollhorizontally(-1) || mhorizontalrecyclerview.gettranslationx() < 0) {
                        //偏移值加上已偏移的值
                        float transx = mdeltax + mhorizontalrecyclerview.gettranslationx();
                        if (mhorizontalrecyclerview.canscrollhorizontally(-1) && transx >= 0) {
                            transx = 0;
                        }

                        //rv和showmore一起设置-translationx
                        mhorizontalrecyclerview.settranslationx(transx);
                        sethinttexttranslationx(mdeltax);
                    }
                } else if (mdeltax < 0) {
                    // 左滑并判断是否滑动到边缘
                    if (!mhorizontalrecyclerview.canscrollhorizontally(1) || mhorizontalrecyclerview.gettranslationx() > 0) {
                        //偏移值加上已偏移的值
                        float transx = mdeltax + mhorizontalrecyclerview.gettranslationx();
                        if (transx <= 0 && mhorizontalrecyclerview.canscrollhorizontally(1)) {
                            transx = 0;
                        }
                        //rv和showmore一起设置-translationx
                        mhorizontalrecyclerview.settranslationx(transx);
                        sethinttexttranslationx(mdeltax);
                    }
                }

                break;
            case motionevent.action_cancel:
            case motionevent.action_up:

                //拦截事件-父布局滚
                getparent().requestdisallowintercepttouchevent(false);

                // 释放动画
                if (mreleasedanim != null && mreleasedanim.isrunning()) {
                    break;
                }

                //如果达到指定位置了才算释放
                if (moffsetwidth != 0 && mhintleftmargin <= moffsetwidth && mlistener != null) {
                    mlistener.onrelease();
                }

                //默认的回去动画
                mreleasedanim = valueanimator.offloat(1.0f, 0);
                mreleasedanim.setduration(300);
                mreleasedanim.addupdatelistener(new valueanimator.animatorupdatelistener() {
                    @override
                    public void onanimationupdate(valueanimator animation) {
                        float value = (float) animation.getanimatedvalue();
                        mhorizontalrecyclerview.settranslationx(value * mhorizontalrecyclerview.gettranslationx());
                        mmoretextview.settranslationx(value * mmoretextview.gettranslationx());
                    }
                });
                mreleasedanim.start();

                break;

        }
        return mhorizontalrecyclerview.gettranslationx() != 0 ? true : super.dispatchtouchevent(ev);
    }
     

    /**
     * 设置showmore布局的偏移量,并且设置内部重绘贝塞尔曲线的控制点变量
     */
    private void sethinttexttranslationx(float deltax) {

        float offsetx = 0;
        if (mmoretextview != null) {
            mhintleftmargin += deltax;
            if (mhintleftmargin <= moffsetwidth) {
                offsetx = moffsetwidth;
                mmoretextview.setverticaltext(release_more);
            } else {
                offsetx = mhintleftmargin;
                mmoretextview.setverticaltext(scroll_more);
            }
            mmoretextview.setshadowoffset(offsetx, moffsetwidth);
            mmoretextview.settranslationx(offsetx);
        }

    }   

    public interface onshowmorelistener {
        void onrelease();
    }

    private onshowmorelistener mlistener;

    public void setonshowmorelistener(onshowmorelistener listener) {
        this.mlistener = listener;
    } 

如果是在一个列表中使用此控件,我们最好还需要处理请求父布局的拦截操作,比如:

        case motionevent.action_move:
  
            float mdeltax = (ev.getrawx() - mlastx);
            float mdeltay = ev.getrawy() - mlasty;

      
            //拦截事件-让我滚
            getparent().requestdisallowintercepttouchevent(true);

这行不行,行!但是这么已拦截当按在这个控件上往上下滑动的时候,同样不能生效,会导致断触的效果。所以我们需要让拦截事件只拦截水平方向的事件。

并且为了兼容处理,有些设备第一次触摸的时候,mdeltax 与 mdeltay 都为 0,从而无法拦截,所以我们需要做个判断,非第一次触摸才开始拦截。

        switch (ev.getaction()) {
            case motionevent.action_down:
                mhintleftmargin = 0;
                mmoveindex = 0;
                isfirstmove = true;
                mlastx = ev.getrawx();
                mlasty = ev.getrawy();
                break;
            case motionevent.action_move:
                // 释放动画
                if (mreleasedanim != null && mreleasedanim.isrunning()) {
                    break;
                }
                float mdeltax = (ev.getrawx() - mlastx);
                float mdeltay = ev.getrawy() - mlasty;

                if (isfirstmove) {
                    // 处理事件冲突
                    if (math.abs(mdeltax) > math.abs(mdeltay)) {
                        //拦截事件-让我滚
                        getparent().requestdisallowintercepttouchevent(true);
                    } else {
                        //拦截事件-父布局滚
                        getparent().requestdisallowintercepttouchevent(false);
                    }
                }

                mmoveindex++;

                if (mmoveindex > 2) {
                    isfirstmove = false;
                }

                mlastx = ev.getrawx();
                mlasty = ev.getrawy();

使用与监听:

        val group5 = findviewbyid<viewgroup5>(r.id.viewgroup5)

        val recyclerview = findviewbyid<recyclerview>(r.id.recycler_view)

        recyclerview.horizontal().binddata(list, r.layout.item_scroll_card) { holder: viewholder, t: string, position: int ->

            holder.settext(r.id.tv_name, "测试数据 $t")
        }

        group5.setonshowmorelistener {
            toast("进入更多的页面")
        }

效果:

如果嵌套会怎样?

如果和豆瓣的滑动效果与闲鱼的滑动进入详情效果放在一起:

当我们滑动正常布局可以触发闲鱼的滑动,当我们滑动豆瓣的滑动详情,则是请求父布局不要拦截,可以正常的触发滑动的效果,确实也符合我们的预期。

后记

其实本文并没有什么新的知识点,无非就是在 viewgroup 的测量布局的基础上,加上事件的处理,从易到难实现各种右滑进入详情的效果。

只是需要注意的是,事件的处理与滑动有多种组合的方式实现,我们还是需要按需选择,比如有一些处理滑动冲突的情况,最好我们还是使用 motionevent + scroller / settranslation 实现,对于一些不复杂的页面我们可以使用谷歌封装好的 viewdraghelper 来快速实现。

当然类似的效果也并不是只有自定义viewgroup可以实现,其他的类似 behavor 也能实现同样的效果,但我认为它并不属于自定义view体系,是另一个概念了,所以并没有对它有过多的介绍。如果真要扩展开来要讲的东西也太多了。

以上就是android自定义viewgroup实现右滑进入详情的详细内容,更多关于android viewgroup的资料请关注七九推其它相关文章!

(0)
打赏 微信扫一扫 微信扫一扫

相关文章:

  • codeforces1373F. Network Coverage

    codeforces1373F. Network Coverage

    可以二分b[1]给a[1]的值,看了standing学了种O(n)的办法,每次求a[i]-b[i-1],若大于0,则累加到下一次(a[i]只能有b[i]和b[i... [阅读全文]
  • react生命周期(类组件/函数组件)操作代码

    react生命周期(类组件/函数组件)操作代码

    1.react代码模式分为两种 类组件和函数组件(生命周期也有所不同)2.类组件(写法如下)import react from 'react'export de... [阅读全文]
  • Android UI动态设置带有Stroke渐变色背景Drawable

    Android UI动态设置带有Stroke渐变色背景Drawable

    摘要每天一个ui小技巧,提高开发效率 ui开发中繁琐的drawable xml开发,不同的view背景样式一致,却因为部分设计区别,就要重新写一套新的xml d... [阅读全文]
  • Android实现TV端大图浏览效果的全过程

    Android实现TV端大图浏览效果的全过程

    前言最近tv开发需要加载的图片很长,大小也很大,并且还不允许压缩。比如显示:世界地图、清明上河图、微博长图、海报、活动照片等。那么对于这种需求,该如何做呢?首先... [阅读全文]
  • JetpackCompose Navigation导航实现流程

    JetpackCompose Navigation导航实现流程

    navigation 快速上手下面案例简要展示使用 compose 版本的 navigation 库来实现两个页面之间的跳转这是完整的结构(忽略掉红线划过的那个... [阅读全文]
  • 一文带你掌握JPA实体类注解

    一文带你掌握JPA实体类注解

    基本注解@entity标注于实体类声明语句之前,指出该 java 类为实体类,将映射到指定的数据库表。name(可选):实体名称。 缺省为实体类的非限定名称。该... [阅读全文]

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2023  七九推 保留所有权利. 粤ICP备17035492号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com