声明:本文作者授权微信公众号非著名程序员(smart_android)在微信公众号平台原创首发此文章。
目标:实现一个点击删除的Item
效果图如下:我知道作为研发肯定会吐槽这个删除的设计,但是我还是要上图,不然我们岂不是不明真相的吃瓜群众…
- 两个删除的按钮
- 点击减号出现垃圾桶
- 点击垃圾桶删除当前的item
- 出现垃圾桶,点击空白处回到初始状态显示减号
虽然这个交互个人不太赞同,但是并表示我们实现不了,PS产品还说了,不要支持滑动,都用点击来交互(我了啦个F*CK)
下面我们就一步步实现这view
实现方法有很多种,这个东西不是特别难,其中涉及到一些小的计算思路和实现思路给大家分享下;
- 你可以用布局上下层嵌套写到xml里面来实现,这是最简单的实现
- 你可以用LayoutInfater填充上下层view来组合出来这view,然后加动画
- 我们还可以自定义一个Layout提供设置姓名的方法,和删除的事件出去,剩下的动作在view内部完成
国家惯例,先看完成的效果图,
再来一个动态图吧,没GIF没真相;
- 从效果图分析,我们需要提供的属性有一下几种
<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来实现这个小东西;
- 现在开始着手实现,自定义View继承RelativeLayout
- 在构造中获取我们分析的自定义属性
- 获取到属性后,用代码构造我们的上层View和下层View以及上层的文字控件
- 给构造出来的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();
}
- 添加底部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);
}
- 添加上层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();
}
}
});
}
}
}