Android自定义控件:左滑删除itemRecyclerView,ListView,GrdiView通配,教你如何最快最轻松定制,而不是复制粘贴!

这里我不直接贴大段代码,不直接讲用的什么什么技术,什么什么框架,从设计思路出发,到难点剖析,以最基础也是最快速的方式搞定他。(这里未借鉴任何网上代码,全是自己想自己手撸的,其实真的不难)


思路:


你需要重写哪个控件?

是recyclerview还是包含item的relativelayout或者是lineralayout?我个人比较崇尚重用性,为了在任何下拉控件中都能使用,我选择了重写item。而且重写有个很严重的问题,他本身就自带很多手势,你再添加手势,不崩就已经很好了。


重写item的具体思路?


这里的黄色区域就是我们重写的relativelayout,蓝色的就是代表删除按钮textview,也可以是button。最外面的布局是FrameLayout,因为这个布局可以实现后面的控件覆盖前面的控件,看代码。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="80dp">

    <TextView
        android:id="@+id/tv_delete"
        android:layout_width="80dp"
        android:layout_height="match_parent"
        android:layout_gravity="end"
        android:background="@color/colorAccent"
        android:text="删除"/>

    <com.example.test.HorizontalSlideView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/tv_main"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#FFFFFF"/>

    </com.example.test.HorizontalSlideView>

</FrameLayout>
这样显示出来的是上面那个删除按钮被下面的总布局覆盖了。


课外知识:为什么用relativelayout?

因为他在控件多的时候,可以通过比linearlayout更精确的定位规则,减少嵌套,就是不在relativelayout里嵌套另外的布局就可以轻松实现定位,嵌套一少的话性能就会大大增加,因为在视图绘制的时候,层级越深,需要的性能是更多更多的。


如何重写这个relativelayout?


你需要先学会实现拖动功能,就是学会自定义一个可拖动的控件,我们这里举个例子。

我们唯一需要做的就是,1.创建一个继承自relativelayout的类

2.重写onTouchEvent,这个是监听手势的。

@Override
public boolean onTouchEvent(MotionEvent event) {

    //这是可以获取手势的方法,我们需要重写它,它来自View类
    
    int x = (int) event.getX();//event就是类似手势信息了,这里获取了你触摸的X坐标
    int y = (int) event.getY();

    switch (event.getAction()) {

        case MotionEvent.ACTION_DOWN://这里写你的触摸时按下那一刻的操作

            lastX = x;//我们这里记录下手指初始的落点
            lastY = y;

            break;

        case MotionEvent.ACTION_MOVE://这里写你的手指在移动时的操作,这个方法在移动中会执行很多次

            int offsetX = x - lastX;//这里是手指的当前位置 和 初始落点之差 通过偏移量来改变位置
            int offsetY = y - lastY;

            //getLeft是你这个控件左边的横坐标,以此类推,通过不断把差值赋给layout,来重新放置你这个控件
            layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

            break;

    }

    return true;

}
注释的很清楚,代码也简单,可以迅速弄懂。


这个控件的全部代码

public class TestView extends RelativeLayout{

    public TestView(Context context) {
        super(context);
    }

    public TestView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public TestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    int lastX;
    int lastY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        //这是可以获取手势的方法,我们需要重写它,它来自View类

        int x = (int) event.getX();//event就是类似手势信息了,这里获取了你触摸的X坐标
        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN://这里写你的触摸时按下那一刻的操作

                lastX = x;//我们这里记录下手指初始的落点
                lastY = y;

                break;

            case MotionEvent.ACTION_MOVE://这里写你的手指在移动时的操作,这个方法在移动中会执行很多次

                int offsetX = x - lastX;//这里是手指的当前位置 和 初始落点之差 通过偏移量来改变位置
                int offsetY = y - lastY;

                //getLeft是你这个控件左边的横坐标,以此类推,通过不断把差值赋给layout,来重新放置你这个控件
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

                break;

        }

        return true;

    }
}

这个控件在xml中的使用代码(注意,这里不要粘贴,你的这个包名<com.example.test.TestView不能跟我一样,按照你建的路径来)

<com.example.test.TestView
    android:layout_width="30dp"
    android:layout_height="30dp"
    android:background="@color/colorAccent"/>

效果




这个会了已经是成功了一半了。因为我们的需求是水平滑动,所以你下一步需要把它修改为仅限于水平范围内的滑动。

//getLeft是你这个控件左边的横坐标,以此类推,通过不断把差值赋给layout,来重新放置你这个控件
//全屏幕滑动
//layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

//水平滑动
layout(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom());
你只需要修改一处代码就可以了,就是去掉offsetY,很简单。


效果




接下来要学会在指定区域内滑动


因为你要实现的是把它挪开,让删除按钮暴露出来,所以你滑多了不行,向右滑也不行。


所以怎么才能实现在指定范围内滑动呢?

1.你需要获取你这个自定义控件的宽度

2.你需要获取那个删除按钮的宽度

3.给layout加一个判断语句即可



获取你这个自定义控件的宽度

定义一个成员变量,十分简单

//全长
ITEM_WIDTH = getWidth();

获取删除按钮的宽度

这个不是太简单,因为这个控件在我们上面展示xml布局的FragmLayout里,所以需要先从当前控件,获取这个FrameLayout,再根据这个FrameLayout去获取这个删除按钮即可

//按钮宽度

//获取父亲,这是view类里的方法,即FrameLayout
ViewGroup vg = (ViewGroup) getParent();
//由FrameLayout获取儿子,即那个删除按钮,为什么传入的是0,你可以看一下上面我们xml那个布局的代码先后顺序
View v = vg.getChildAt(0);
DELETE_BUTTON_WIDTH = v.getWidth();

所以这样去限定范围

//水平滑动并且在一定范围内
if (getRight() + offsetX < ITEM_WIDTH && getRight() + offsetX >= ITEM_WIDTH - DELETE_BUTTON_WIDTH) {

    layout(getLeft() + offsetX, getTop(),
            getRight() + offsetX, getBottom());

}



我们这里为了测试,就简单限定了一下范围,300到500之间

效果图


好了,到此为止,已经可以说是做完了!其实并不难!接下来再抠一两个细节就完工了。



第一个:你可以看一下你的QQ,滑的过程中松手了,如果你的右边缘在删除按钮的右半部分,就会复位,如果你的右边缘在删除按钮的左半部分,就会自动完成剩下的位移。


怎么实现?

我们需要给手势添加一个

case MotionEvent.ACTION_UP:
这个指代的是当你触摸完后,手指离开触摸屏的那一刹那,可以看看我之前几张gif,就是我松开鼠标的那一刻。

有了这个思路就清晰了,用我们之前的

getRight()
获取右边缘位置,判断在哪个区间就可以了,非常简单。


代码如下

case MotionEvent.ACTION_UP:


    //向右复位
    if (getRight() <= ITEM_WIDTH && getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH / 2) {

        ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start();

        //向左复位
    } else if (getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH && getRight() < ITEM_WIDTH - (DELETE_BUTTON_WIDTH / 2)) {

        ObjectAnimator.ofFloat(this, "translationX", 0, -(getRight() - (ITEM_WIDTH - DELETE_BUTTON_WIDTH))).setDuration(600).start();
        
    }


    break;

上个坐标图


就是根据坐标来写if语句的。


然后if语句里面的

ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start();
就是属性动画,非常简单好用,我来讲一下参数。

this:this就是当前这个类,就是这个自定义控件

translationX就代表动画执行的是X轴上平移的操作

0就是从你的当前坐标开始动,如果你设置另外的坐标可能不会太好

ITEM_WIDTH - getRight()就是你的移动距离 我们这里的是向右复位操作,这个距离很容易的出来。


setDuration(600)就代表动画执行时间是600ms

最后start启动动画


为什么我们要用 属性动画 实现位置的复原而不是用之前的layout来重新放置控件的位置?

因为自动复位不再是你的手拖着控件走了,而是他自己复原,如果你用layout,会瞬间回到原点,会造成非常不友好的用户体验。

(别人说的用Scroller也不是不可以,但是那个也是基于的动画而且相比属性动画较为复杂,所以属性动画要强大简单的多)


所以现在的效果图是这样的(不再是那个测试用例了,请读者注意,布局变成最开始的xml布局了)




这个搞定了以后,我们剩下了最后一个难点,如何实现同时只能滑动一个。


为什么难?

因为我们重写的是item,我们手哪里有这么长去管到外面的事情?我想了很多方法,观察者,接口回调,发消息,单例模式等等等。而且我看了网上别人的实现,大部分都写在viewholder里,我觉得耦合度太高了,所以我采用了一个简单的办法

内部维护一个当前自定义控件的实例。


听起来很高大上,其实就是在MainActivity中设置两个静态变量即可。

public static HorizontalSlideView current;

public static boolean isDeleteIconShown;
再发一下控件中增加的代码,仔细讲一讲这个还算不错的方法。


case up中增加了2行代码

case MotionEvent.ACTION_UP:


    //向右复位
    if (getRight() <= ITEM_WIDTH && getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH / 2) {

        ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start();

        //向左复位
    } else if (getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH && getRight() < ITEM_WIDTH - (DELETE_BUTTON_WIDTH / 2)) {

        ObjectAnimator.ofFloat(this, "translationX", 0, -(getRight() - (ITEM_WIDTH - DELETE_BUTTON_WIDTH))).setDuration(600).start();

        
        MainActivity.isDeleteIconShown = true;
MainActivity.current = this; } break;
在完成向左复位的时候,我们是松开了手,未滑完的动画帮你完成了,这个时候自然是处于已显示状态了。


所以第一行:把已显示状态置为true

第二行:把你这个自定义控件类当前的实例传进去。是和MainActivity中的量是一一对应的的。


最后在case down里增加一个判断就完工了

case MotionEvent.ACTION_DOWN:

    if (isDeleteIconShown) {

        if (MainActivity.current == this) {

            ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start();
            isDeleteIconShown = false;

        }

        return false;

    }

    lastX = x;
    lastY = y;

    break;
如果当前状态是已显示状态,就要进入这个if。

再进行判断,我们的这个实例是不是和MainActivity中的实例一样(这样==的比较,实际在比较两个实例的空间地址是否一样,所以如果是不同的实例,自然是不一样的)


最后那个return false就是拦截你后面的操作,不让后面的操作生效。这是涉及到了Android事件分发机制,如果有兴趣的可以看看我的另外一篇文章http://blog.csdn.net/qq_36523667/article/details/78627595

也是一目了然。


好了,大功告成。掌握了方法步骤,接下来你们就可以随意地定制了!下面贴上全部代码和xml


自定义控件

public class HorizontalSlideView extends RelativeLayout {

    public HorizontalSlideView(Context context) {
        super(context);
    }

    public HorizontalSlideView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public HorizontalSlideView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public HorizontalSlideView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    int lastX;
    int lastY;

    int ITEM_WIDTH;
    int DELETE_BUTTON_WIDTH;

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:

                if (isDeleteIconShown) {

                    if (MainActivity.current == this) {

                        ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start();
                        isDeleteIconShown = false;

                    }

                    return false;

                }

                lastX = x;
                lastY = y;

                break;

            case MotionEvent.ACTION_MOVE:

                int offsetX = x - lastX;
                int offsetY = y - lastY;

                //全长
                ITEM_WIDTH = getWidth();
                //按钮宽度
                ViewGroup vg = (ViewGroup) getParent();
                DELETE_BUTTON_WIDTH = ((ViewGroup) getParent()).getChildAt(0).getWidth();

                if (getRight() + offsetX < ITEM_WIDTH && getRight() + offsetX >= ITEM_WIDTH - DELETE_BUTTON_WIDTH) {

                    layout(getLeft() + offsetX, getTop(),
                            getRight() + offsetX, getBottom());

                }

                break;

            case MotionEvent.ACTION_UP:


                //向右复位
                if (getRight() <= ITEM_WIDTH && getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH / 2) {

                    ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start();

                    //向左复位
                } else if (getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH && getRight() < ITEM_WIDTH - (DELETE_BUTTON_WIDTH / 2)) {

                    ObjectAnimator.ofFloat(this, "translationX", 0, -(getRight() - (ITEM_WIDTH - DELETE_BUTTON_WIDTH))).setDuration(600).start();

                    isDeleteIconShown = true;

                    MainActivity.current = this;

                }


                break;

        }

        return true;//这就是继续要使用触摸指令,如果false 将继续寻找要使用它的人

    }

}

MainActivity

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.rv)
    RecyclerView mRv;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        LinearLayoutManager manager = new LinearLayoutManager(this);
        CommonAdapter<String> adapter =
                new CommonAdapter<String>(
                        this,
                        R.layout.item,
                        initList("开源资讯", "推荐博客", "技术问答", "每日一博")) {
                    @Override
                    protected void convert(final ViewHolder holder, String s, int position) {
                        TextView mTv = holder.getView(R.id.tv_main);
                        mTv.setText(s);
                    }
                };

        mRv.setLayoutManager(manager);
        mRv.setAdapter(adapter);

    }


    public static HorizontalSlideView current;

    public static boolean isDeleteIconShown;

    private <T> List<T> initList(T... ts) {

        List<T> list = new ArrayList<>();

        Collections.addAll(list, ts);

        return list;

    }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

item.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="80dp">

    <TextView
        android:id="@+id/tv_delete"
        android:layout_width="80dp"
        android:layout_height="match_parent"
        android:layout_gravity="end"
        android:background="@color/colorAccent"
        android:text="删除"/>

    <com.example.test.HorizontalSlideView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/tv_main"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#FFFFFF"/>

    </com.example.test.HorizontalSlideView>

</FrameLayout>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值