Android_自定义删除View

声明:本文作者授权微信公众号非著名程序员(smart_android)在微信公众号平台原创首发此文章。


目标:实现一个点击删除的Item


效果图如下:我知道作为研发肯定会吐槽这个删除的设计,但是我还是要上图,不然我们岂不是不明真相的吃瓜群众…


这里写图片描述


  1. 两个删除的按钮
  2. 点击减号出现垃圾桶
  3. 点击垃圾桶删除当前的item
  4. 出现垃圾桶,点击空白处回到初始状态显示减号
    虽然这个交互个人不太赞同,但是并表示我们实现不了,PS产品还说了,不要支持滑动,都用点击来交互(我了啦个F*CK)

下面我们就一步步实现这view
实现方法有很多种,这个东西不是特别难,其中涉及到一些小的计算思路和实现思路给大家分享下;

  1. 你可以用布局上下层嵌套写到xml里面来实现,这是最简单的实现
  2. 你可以用LayoutInfater填充上下层view来组合出来这view,然后加动画
  3. 我们还可以自定义一个Layout提供设置姓名的方法,和删除的事件出去,剩下的动作在view内部完成

国家惯例,先看完成的效果图,
这里写图片描述
这里写图片描述

再来一个动态图吧,没GIF没真相;
这里写图片描述


  1. 从效果图分析,我们需要提供的属性有一下几种
    <declare-styleable name="ATScrollDeleteView">
    //上层颜色
        <attr name="top_layer_color" format="color"/>
        // 上层图标
        <attr name="top_layer_icon" format="reference"/>
        // 上层文字
        <attr name="top_layer_desc" format="string"/>
        // 文字颜色
        <attr name="top_layer_desc_color" format="color"/>
        // 文字大小
        <attr name="top_layer_desc_size" format="integer"/>
        // 文字和上层图标的间距
        <attr name="top_layer_icon_margin_desc" format="dimension"/>
        // 上层图标距离左边的距离
        <attr name="top_layer_icon_margin_left" format="dimension"/>
        // 下层颜色
        <attr name="under_layer_color" format="color"/>
        // 下层图标
        <attr name="under_layer_icon" format="reference"/>
    </declare-styleable>
  • 实现思路;分析完需要的属性后,不要着急写代码,我们从效果图分析,下怎么写可以最方便的实现这个View;
    很多人一看到上下两层的第一进入脑袋中的思路是不是

FrameLayout

但是如果要把上下层用代码实现,FrameLayout的params不如

RelativeLayout

好用,这里我选择了RL来实现这个小东西;

  1. 现在开始着手实现,自定义View继承RelativeLayout
  2. 在构造中获取我们分析的自定义属性
  3. 获取到属性后,用代码构造我们的上层View和下层View以及上层的文字控件
  4. 给构造出来的view暴漏事件给外部,方便界面中使用

// 获取自定义属性,这代码估计大家都看吐了吧,
  public ATScrollDeleteView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ATScrollDeleteView, defStyleAttr, R.style.def_scroll_delete_style);
        int indexCount = typedArray.getIndexCount();
        for (int i = 0; i < indexCount; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.ATScrollDeleteView_top_layer_color:
                    topLayerColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_icon:
                    topLayerIcon = typedArray.getResourceId(attr, 0);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_desc:
                    topLayerDesc = typedArray.getString(attr);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_desc_color:
                    topLayerDescColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_desc_size:
                    topLayerDescSize = typedArray.getInteger(attr, 0);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_icon_margin_left:
                    topLayerIconMarginLeft = typedArray.getDimensionPixelOffset(attr, 0);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_icon_margin_desc:
                    topLayerIconMarginDesc = typedArray.getDimensionPixelOffset(attr, 0);
                    break;
                case R.styleable.ATScrollDeleteView_under_layer_color:
                    underLayerColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.ATScrollDeleteView_under_layer_icon:
                    underLayerIcon = typedArray.getResourceId(attr, 0);
                    break;
            }
        }
        typedArray.recycle();
        // 顺手把我们第二层的颜色给设置了
        setBackgroundColor(underLayerColor);
    }

  • 确定View的比例以及尺寸然后构造上下层的View
 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        viewHeight = h;
        viewWidth = w;
        // 静态内部类提供记录View的状态,展开还是合并
        viewState = ViewState.FOLD;
        addUnderIconView(w);
        addTopLayerView();
        calculateOffSideDistance();
        addViewListener();
    }
  1. 添加底部icon的View
    我们在构造中绘制了底部的颜色,现在只需要构造一个Image然后add到view中即可
    private void addUnderIconView(int w) {
    // 注意view展开的比例,我们的垃圾桶图标正好在展开的比例的尺寸中居中
        underIconView = new ImageView(getContext());
        underIconView.setImageResource(underLayerIcon);
        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        params.addRule(RelativeLayout.CENTER_VERTICAL);
        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
        Bitmap underBitmap = BitmapFactory.decodeResource(getResources(), underLayerIcon);
        int underBitmapWidth = 0;
        if (null != underBitmap) {
            underBitmapWidth = underBitmap.getWidth();
        }
        // 计算展开的比例,以及image的尺寸使其居中
        params.rightMargin = (int) (w * TOUCH_SCROLL_SCALE / 2) - underBitmapWidth / 2;
        underIconView.setLayoutParams(params);
        addView(underIconView);
    }
  1. 添加上层View
    我们观察下上层不是一个view,他是由一个上层布局,上层布局中有一个textview结合完成的;
  private void addTopLayerView() {
  // 上层的父布局,承载Textview
        topLayerParent = new LinearLayout(getContext());
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        topLayerParent.setLayoutParams(params);
        topLayerParent.setBackgroundColor(topLayerColor);
        // textview显示文字用的
        topDescView = new TextView(getContext());
        topDescView.setTextSize(TypedValue.COMPLEX_UNIT_SP, topLayerDescSize);
        topDescView.setTextColor(topLayerDescColor);
        topDescView.setText(topLayerDesc);
        // 文字中的图标就是我们的减号这个属性是可以配置的topDescView.setCompoundDrawablePadding(topLayerIconMarginDesc);
        LinearLayout.LayoutParams descLp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        descLp.gravity = Gravity.CENTER_VERTICAL;
        descLp.leftMargin = topLayerIconMarginLeft;
        topDescView.setGravity(Gravity.CENTER);
        topDescView.setLayoutParams(descLp);
        Drawable topIconDrawable = getResources().getDrawable(topLayerIcon);
        if (null != topIconDrawable) {
            topIconDrawable.setBounds(0, 0, topIconDrawable.getMinimumWidth(), topIconDrawable.getMinimumHeight());
            topDescView.setCompoundDrawables(topIconDrawable, null, null, null);
        }
        // 添加文字view到上层view
        topLayerParent.addView(topDescView);
        // 添加上层view到自定义view中
        addView(topLayerParent);
    }

到此为止我们已经实现了View的80%的工作了剩下的就是提供暴漏设置名字,和删除的事件,以及让我们的上层View动起来,
剩下的事情就比较简单了,我们提供一个属性动画让上层VIew沿着X轴运动,
这里需要注意的移动的距离以及textview的联动
?????为什么呢是这样的,因为我们垃圾桶的显示比例和上层View的文字控件并不是一个宽度,我们的如果不处理TextView的联动,会出现什么问题????
没错,就是垃圾桶全部显示的时候,textview会被隐藏一部分,但是如果textview的坐标位置,和垃圾桶的宽度一致的话,在折叠状态textview太靠右边,巨丑无比……各位好好理解下,所以我们为什么一开始就提供了两个属性一个是上层icon的距离,文字的距离还有一个上层icon距离左边的距离,我们用着两个距离结合垃圾桶的距离计算出来,当垃圾桶全部展开的时候,上层icon正好完全隐藏,而且文字正好是全部展示的这个距离


说一千道一万,看代码直接,特别简单,我们拿到上层view的图片宽度,加上上层icon距离左边的距离就是这个间距

    private int calculateOffSideDistance() {
        Bitmap topLayerBitmap = BitmapFactory.decodeResource(getResources(), topLayerIcon);
        if (null != topLayerBitmap) {
            return topLayerBitmap.getWidth() + topLayerIconMarginLeft;
        }
        return 0;
    }

下面就是我们暴露出来的公开事件,

// 给View内部的名字变量赋值
  public void setScrollDeleteDesc(String desc) {
        this.desc = desc;
    }

    public interface OnScrollDeleteListener {
        void deleteAction();
    }
// view内部的垃圾桶点击事件传递给activity
    public void setScrollDeleteListener(final OnScrollDeleteListener scrollDeleteListener) {
        this.scrollDeleteListener = scrollDeleteListener;
    }

ok,剩下的就是点击的时候,我们需要让上层View和文字的View 联动就大功告成了

下面我们看下view的动画代码

    private void showOrHideUnderLayer(boolean isShow) {
    // 根据传递过来的变量判断是展开动画还是折叠动画,如果是展开动画,从0开始到垃圾桶的宽度的负数值,因为要跑到屏幕外面显示垃圾桶,并且隐藏减号图标,对吧,然后回来的时候相反,显示减号和文字,隐藏垃圾桶,
        int startX = isShow ? 0 : (int) (-viewWidth * TOUCH_SCROLL_SCALE);
        int offSide = (int) (isShow ? -viewWidth * TOUCH_SCROLL_SCALE : 0);
        topLayerParent.setClickable(false);
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(topLayerParent, "translationX", startX, offSide);
        objectAnimator.setDuration(ANIMATION_TIME);
        objectAnimator.setInterpolator(new LinearInterpolator());
        objectAnimator.start();
        objectAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                topLayerParent.setClickable(true);
            }
        });
// 这块就是文字的联动,当显示垃圾桶的时候,外面需要移动文字,隐藏减号图标,那么textview像左滑动的距离是多少,就是上面外面计算出来的减号的宽度加上icon的边距
        int topTextStartX = isShow ? 0 : (int) (viewWidth * TOUCH_SCROLL_SCALE - calculateOffSideDistance());
        int topTextoffSide = isShow ? (int) (viewWidth * TOUCH_SCROLL_SCALE - calculateOffSideDistance()) : 0;

        topDescView.setClickable(false);
        ObjectAnimator topTextAnim = ObjectAnimator.ofFloat(topDescView, "translationX", topTextStartX, topTextoffSide);
        topTextAnim.setDuration(ANIMATION_TIME);
        topTextAnim.setInterpolator(new LinearInterpolator());
        topTextAnim.start();
        topTextAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                topDescView.setClickable(true);
            }
        });
    }

到此这个view就分析完毕了,这里还有一个知识点给大家分享下,外面提供的设置文字内容和暴漏的事件的方法,如果直接调用是不会生效的,假如我们不在view内部处理一些东西的话,
大家都知道,我们findViewByID后直接获取view的宽高,是不是获取不到,都是0,
那是因为,但是我们要是写一个延时,5秒再获取宽高就能获取到了,所以我们难道要这样处理吗,那肯定不行啊,他自己处理完毕后会在一个生命周期中通知我们的,就是他会告诉activity,我OK了你可以来获取我的宽高了,想必大家都知道这个方法吧,

onWindowFocusChanged

就是他,这就是为什么我们提供的设置内容的方法,只是记录了外界提供的值,并没直接调用 textview.settext(),如果直接后面加上这一句会发生事情,大家肯定都知道的,
直接就crash了,textview是null,这块要注意下,于是我们改成这样的写法

  @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        // 如果textview初始化完毕,拿到我们记录的内容设置
        if (null != topDescView) {
            topDescView.setText(desc);
        }
        // 如果垃圾桶准备完毕了, 拿到外界设置的回调来给外界提供监听
        if (null != underIconView) {
            underIconView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (null != scrollDeleteListener) {
                        scrollDeleteListener.deleteAction();
                    }
                }
            });
        }
    }

好了最后一个坑也填平了,虽然这个view没有很炫酷的动画**

,但是也没有

**很高深的源码分析

不要感觉啥都没学到,这样的轮子网上多的去了**

,自己造的轮子,能填补一些知识忙点!

**
下面我把这个view的代码全部防到这里,有兴趣的可以放到AS里面跑跑看看
有能力的可以自己加上手势滑动,手势这块我假如有时间加上也会更新,

荆轲刺秦王__The END

public class ATScrollDeleteView extends RelativeLayout {
    private static final float TOUCH_SCROLL_SCALE = 1 / 5.F;
    private static final int ANIMATION_TIME = 300;
    private int topLayerColor;
    private int topLayerIcon;
    private String topLayerDesc;
    private int topLayerDescColor;
    private int topLayerDescSize;
    private int topLayerIconMarginDesc;
    private int topLayerIconMarginLeft;
    private int underLayerColor;
    private int underLayerIcon;

    private int viewWidth;
    private int viewHeight;

    private ImageView underIconView;
    private TextView topDescView;
    private LinearLayout topLayerParent;
    private int viewState;
    private String desc;
    private OnScrollDeleteListener scrollDeleteListener;

    public ATScrollDeleteView(Context context) {
        this(context, null);
    }

    public ATScrollDeleteView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ATScrollDeleteView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ATScrollDeleteView, defStyleAttr, R.style.def_scroll_delete_style);
        int indexCount = typedArray.getIndexCount();
        for (int i = 0; i < indexCount; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.ATScrollDeleteView_top_layer_color:
                    topLayerColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_icon:
                    topLayerIcon = typedArray.getResourceId(attr, 0);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_desc:
                    topLayerDesc = typedArray.getString(attr);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_desc_color:
                    topLayerDescColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_desc_size:
                    topLayerDescSize = typedArray.getInteger(attr, 0);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_icon_margin_left:
                    topLayerIconMarginLeft = typedArray.getDimensionPixelOffset(attr, 0);
                    break;
                case R.styleable.ATScrollDeleteView_top_layer_icon_margin_desc:
                    topLayerIconMarginDesc = typedArray.getDimensionPixelOffset(attr, 0);
                    break;
                case R.styleable.ATScrollDeleteView_under_layer_color:
                    underLayerColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.ATScrollDeleteView_under_layer_icon:
                    underLayerIcon = typedArray.getResourceId(attr, 0);
                    break;
            }
        }
        typedArray.recycle();
        setBackgroundColor(underLayerColor);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        viewHeight = h;
        viewWidth = w;
        viewState = ViewState.FOLD;
        addUnderIconView(w);
        addTopLayerView();
        calculateOffSideDistance();
        addViewListener();
    }

    private int calculateOffSideDistance() {
        Bitmap topLayerBitmap = BitmapFactory.decodeResource(getResources(), topLayerIcon);
        if (null != topLayerBitmap) {
            return topLayerBitmap.getWidth() + topLayerIconMarginLeft;
        }
        return 0;
    }

    private void addViewListener() {
        topDescView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                viewFoldOrNot();
            }
        });

        topLayerParent.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (ViewState.FOLD == viewState) {
                    return;
                }
                viewFoldOrNot();
            }
        });
    }

    private void viewFoldOrNot() {
        if (ViewState.FOLD == viewState) {
            showOrHideUnderLayer(true);
            viewState = ViewState.UN_FOLD;
        } else {
            showOrHideUnderLayer(false);
            viewState = ViewState.FOLD;
        }
    }

    private void showOrHideUnderLayer(boolean isShow) {
        int startX = isShow ? 0 : (int) (-viewWidth * TOUCH_SCROLL_SCALE);
        int offSide = (int) (isShow ? -viewWidth * TOUCH_SCROLL_SCALE : 0);
        topLayerParent.setClickable(false);
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(topLayerParent, "translationX", startX, offSide);
        objectAnimator.setDuration(ANIMATION_TIME);
        objectAnimator.setInterpolator(new LinearInterpolator());
        objectAnimator.start();
        objectAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                topLayerParent.setClickable(true);
            }
        });

        int topTextStartX = isShow ? 0 : (int) (viewWidth * TOUCH_SCROLL_SCALE - calculateOffSideDistance());
        int topTextoffSide = isShow ? (int) (viewWidth * TOUCH_SCROLL_SCALE - calculateOffSideDistance()) : 0;

        topDescView.setClickable(false);
        ObjectAnimator topTextAnim = ObjectAnimator.ofFloat(topDescView, "translationX", topTextStartX, topTextoffSide);
        topTextAnim.setDuration(ANIMATION_TIME);
        topTextAnim.setInterpolator(new LinearInterpolator());
        topTextAnim.start();
        topTextAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                topDescView.setClickable(true);
            }
        });
    }

    private void addTopLayerView() {
        topLayerParent = new LinearLayout(getContext());
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        topLayerParent.setLayoutParams(params);
        topLayerParent.setBackgroundColor(topLayerColor);
        topDescView = new TextView(getContext());
        topDescView.setTextSize(TypedValue.COMPLEX_UNIT_SP, topLayerDescSize);
        topDescView.setTextColor(topLayerDescColor);
        topDescView.setText(topLayerDesc);
        topDescView.setCompoundDrawablePadding(topLayerIconMarginDesc);
        LinearLayout.LayoutParams descLp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        descLp.gravity = Gravity.CENTER_VERTICAL;
        descLp.leftMargin = topLayerIconMarginLeft;
        topDescView.setGravity(Gravity.CENTER);
        topDescView.setLayoutParams(descLp);
        Drawable topIconDrawable = getResources().getDrawable(topLayerIcon);
        if (null != topIconDrawable) {
            topIconDrawable.setBounds(0, 0, topIconDrawable.getMinimumWidth(), topIconDrawable.getMinimumHeight());
            topDescView.setCompoundDrawables(topIconDrawable, null, null, null);
        }
        topLayerParent.addView(topDescView);
        addView(topLayerParent);
    }

    private void addUnderIconView(int w) {
        underIconView = new ImageView(getContext());
        underIconView.setImageResource(underLayerIcon);
        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        params.addRule(RelativeLayout.CENTER_VERTICAL);
        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
        Bitmap underBitmap = BitmapFactory.decodeResource(getResources(), underLayerIcon);
        int underBitmapWidth = 0;
        if (null != underBitmap) {
            underBitmapWidth = underBitmap.getWidth();
        }
        params.rightMargin = (int) (w * TOUCH_SCROLL_SCALE / 2) - underBitmapWidth / 2;
        underIconView.setLayoutParams(params);
        addView(underIconView);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

    private static class ViewState {
        private static final int UN_FOLD = 11;
        private static final int FOLD = 12;
    }

    public void setScrollDeleteDesc(String desc) {
        this.desc = desc;
    }

    public interface OnScrollDeleteListener {
        void deleteAction();
    }

    public void setScrollDeleteListener(final OnScrollDeleteListener scrollDeleteListener) {
        this.scrollDeleteListener = scrollDeleteListener;
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (null != topDescView) {
            topDescView.setText(desc);
        }
        if (null != underIconView) {
            underIconView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (null != scrollDeleteListener) {
                        scrollDeleteListener.deleteAction();
                    }
                }
            });
        }
    }
}

源代码下载地址; 工程项目地址,希望大家star一下谢谢:https://github.com/GuoFeilong/ATLoginButton_New

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值