ListView和RecyclerView列表点击反馈
来源:互联网 发布:php log日志类 编辑:程序博客网 时间:2024/04/20 03:58
整理下Android开发中列表点击反馈的一些知识点,就是点击Item会出现背景阴影的效果。
目前最常用的两个列表控件ListView和RecyclerView,可以说RecyclerView作为ListView的升级版有着更强大的功能,现在也基本都使用RecyclerView居多。当然了,RecyclerView还是有些属性设置没有但ListView有,就是下面要说的点击反馈。
ListView
先来说说ListView,ListView默认就带有点击反馈效果,不需要特意去设置。当然了,你如果不想要默认的点击反馈效果或想替换别的样式,都可以通过设置一下属性来实现:
- <!-- 去除ListView的点击反馈效果 -->
- <ListView
- android:listSelector="@android:color/transparent"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
- <!-- 自定义ListView的点击反馈效果 -->
- <ListView
- android:id="@+id/lv_list"
- android:listSelector="@drawable/sel_common_bg_press"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
其中 sel_common_bg_press.xml 文件如下:
- <?xml version="1.0" encoding="utf-8"?>
- <selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_pressed="true">
- <shape>
- <solid android:color="#88ff5722"/>
- </shape>
- </item>
- <item android:drawable="@android:color/transparent"/>
- </selector>
这个很简单,就是定义了按压和没按压时的颜色状态。
基本上ListView的点击反馈都是通过这个属性来处理,不过在一些特殊情况下会出现点击反馈无效的情况:
- 设置了Item布局的android:background属性,并且设置为不透明的背景色,在这种情况下就会看不到点击反馈效果,但实际上android:listSelector属性还是处于作用的状态,只不过被覆盖了看不到。你可以用半透明的背景色设置android:background属性,这种情况还是能看到点击反馈的;
- Item布局里的子视图占满了整个布局,比如ImageView的图片占满整个Item,这时也看不到点击反馈;
- 还有一个不算点击反馈无效的情况就是,如果Item里面的控件接收并处理了点击事件,点击反馈效果也看不到,因为点击事件被别人消耗了;
- <!-- android:drawSelectorOnTop="true"让点击反馈为Item的最顶部 -->
- <ListView
- android:id="@+id/lv_list"
- android:drawSelectorOnTop="true"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
设置了android:drawSelectorOnTop="true"属性后,即便Item布局被其它东西覆盖了也能看到点击反馈效果。
RecyclerView
RecyclerView就没有ListView这么方便了,默认是没有点击反馈的,需要我们自己设置。简单的话可以设置Item布局的android:background属性,这个效果就和ListView的android:listSelector属性效果类似,但和ListView还是会面临同样的点击看不到效果的情况,而且RecyclerView并没有android:drawSelectorOnTop这些相关的属性,所以要另外想办法处理。
这个时候就要用前面提到的android:foreground设置前景色属性啦,不过这个属性只有在android 6.0版本以上或者FrameLayout控件布局上使用才有效果,所以为了兼容低版本,只能选择使用FrameLayout布局来作为Item的顶层布局(该方法同样适用ListView)。使用如下:
- <?xml version="1.0" encoding="utf-8"?>
- <FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:clickable="true"
- android:foreground="@drawable/sel_common_bg_press">
- <ImageView
- android:id="@+id/iv_icon"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:scaleType="centerCrop"
- android:src="@mipmap/ic_pic"/>
- </FrameLayout>
实际也挺简单的,就是让FrameLayout作为最外层布局就行了。有一点需要注意下,我对FrameLayout设置了android:clickable="true"属性,这么做是为了说明FrameLayout要能接收到点击事件才会有点击反馈效果!正常情况我们并不需要在这里设置,因为我们通常会给列表的Item添加点击监听,这样也能看到点击反馈效果。如果你要使用系统默认的点击反馈样式,可以用这个android:foreground="?android:attr/selectableItemBackground"。
到此RecyclerView的点击反馈也能够处理了,下面在讲下自定义布局的点击反馈效果,就拿经典的开源项目RippleEffect来介绍,这个同样适用ListView。
RippleView
注意,我对RippleEffect源码做了些修改,来让效果更贴近平时的使用。我直接贴源码,因为这里主要讲点击反馈,我只在源码的基础上增加了些触摸的判断。
- public class RippleView extends RelativeLayout {
- // 点击加速度
- private static final int RIPPLE_ACCELERATE = 20;
- // 5种触摸状态
- private static final int RIPPLE_NORMAL = 0;
- private static final int RIPPLE_SINGLE = 1;
- private static final int RIPPLE_LONG_PRESS = 2;
- private static final int RIPPLE_ACTION_MOVE = 3;
- private static final int RIPPLE_ACTION_UP = 4;
- // 触摸状态
- private int rippleStatus = RIPPLE_NORMAL;
- // 是否为列表模式,默认为否
- private boolean isListMode;
- // 用户是否滑动的判别距离
- private int touchSlop;
- // 长按时的坐标
- private int lastLongPressX;
- private int lastLongPressY;
- private int WIDTH;
- private int HEIGHT;
- private int frameRate = 10;
- private int rippleDuration = 400;
- private int rippleAlpha = 90;
- private Handler canvasHandler;
- private float radiusMax = 0;
- private boolean animationRunning = false;
- private int timer = 0;
- private int timerEmpty = 0;
- private int durationEmpty = -1;
- private float x = -1;
- private float y = -1;
- private int zoomDuration;
- private float zoomScale;
- private ScaleAnimation scaleAnimation;
- private Boolean hasToZoom;
- private Boolean isCentered;
- private Integer rippleType;
- private Paint paint;
- private Bitmap originBitmap;
- private int rippleColor;
- private int ripplePadding;
- private GestureDetector gestureDetector;
- private final Runnable runnable = new Runnable() {
- @Override
- public void run() {
- invalidate();
- }
- };
- private OnRippleCompleteListener onCompletionListener;
- public RippleView(Context context) {
- super(context);
- }
- public RippleView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context, attrs);
- }
- public RippleView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- init(context, attrs);
- }
- /**
- * Method that initializes all fields and sets listeners
- *
- * @param context Context used to create this view
- * @param attrs Attribute used to initialize fields
- */
- private void init(final Context context, final AttributeSet attrs) {
- if (isInEditMode())
- return;
- final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RippleView);
- rippleColor = typedArray.getColor(R.styleable.RippleView_rv_color, Color.parseColor("#33626262"));
- rippleType = typedArray.getInt(R.styleable.RippleView_rv_type, 0);
- hasToZoom = typedArray.getBoolean(R.styleable.RippleView_rv_zoom, false);
- isCentered = typedArray.getBoolean(R.styleable.RippleView_rv_centered, false);
- rippleDuration = typedArray.getInteger(R.styleable.RippleView_rv_rippleDuration, rippleDuration);
- frameRate = typedArray.getInteger(R.styleable.RippleView_rv_framerate, frameRate);
- rippleAlpha = typedArray.getInteger(R.styleable.RippleView_rv_alpha, rippleAlpha);
- ripplePadding = typedArray.getDimensionPixelSize(R.styleable.RippleView_rv_ripplePadding, 0);
- canvasHandler = new Handler();
- zoomScale = typedArray.getFloat(R.styleable.RippleView_rv_zoomScale, 1.03f);
- zoomDuration = typedArray.getInt(R.styleable.RippleView_rv_zoomDuration, 200);
- isListMode = typedArray.getBoolean(R.styleable.RippleView_rv_listMode, false);
- typedArray.recycle();
- paint = new Paint();
- paint.setAntiAlias(true);
- paint.setStyle(Paint.Style.FILL);
- paint.setColor(rippleColor);
- paint.setAlpha(rippleAlpha);
- this.setWillNotDraw(false);
- gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
- @Override
- public void onLongPress(MotionEvent event) {
- super.onLongPress(event);
- animateRipple(event);
- sendClickEvent(true);
- lastLongPressX = (int) event.getX();
- lastLongPressY = (int) event.getY();
- rippleStatus = RIPPLE_LONG_PRESS;
- }
- @Override
- public boolean onSingleTapConfirmed(MotionEvent e) {
- return true;
- }
- @Override
- public boolean onSingleTapUp(MotionEvent e) {
- return true;
- }
- });
- this.setDrawingCacheEnabled(true);
- this.setClickable(true);
- this.touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
- }
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
- if (animationRunning) {
- if (isListMode && (rippleStatus == RIPPLE_SINGLE
- || rippleStatus == RIPPLE_ACTION_MOVE || rippleStatus == RIPPLE_ACTION_UP)) {
- doRippleWork(canvas, RIPPLE_ACCELERATE);
- } else {
- doRippleWork(canvas, 1);
- }
- }
- }
- private void doRippleWork(Canvas canvas, int offect) {
- canvas.save();
- if (rippleDuration <= timer * frameRate) {
- // There is problem on Android M where canvas.restore() seems to be called automatically
- // For now, don't call canvas.restore() manually on Android M (API 23)
- canvas.drawCircle(x, y, (radiusMax * (((float) timer * frameRate) / rippleDuration)), paint);
- if (onCompletionListener != null && rippleStatus != RIPPLE_ACTION_MOVE && rippleStatus != RIPPLE_LONG_PRESS) {
- onCompletionListener.onComplete(this);
- }
- if (rippleStatus != RIPPLE_LONG_PRESS) {
- animationRunning = false;
- rippleStatus = RIPPLE_NORMAL;
- timer = 0;
- durationEmpty = -1;
- timerEmpty = 0;
- if (Build.VERSION.SDK_INT != 23) {
- canvas.restore();
- }
- }
- invalidate();
- return;
- } else
- canvasHandler.postDelayed(runnable, frameRate);
- if (timer == 0)
- canvas.save();
- canvas.drawCircle(x, y, (radiusMax * (((float) timer * frameRate) / rippleDuration)), paint);
- paint.setColor(Color.parseColor("#ffff4444"));
- if (rippleType == 1 && originBitmap != null && (((float) timer * frameRate) / rippleDuration) > 0.4f) {
- if (durationEmpty == -1)
- durationEmpty = rippleDuration - timer * frameRate;
- timerEmpty++;
- final Bitmap tmpBitmap = getCircleBitmap((int) ((radiusMax) * (((float) timerEmpty * frameRate) / (durationEmpty))));
- canvas.drawBitmap(tmpBitmap, 0, 0, paint);
- tmpBitmap.recycle();
- }
- paint.setColor(rippleColor);
- if (!isListMode) {
- if (rippleType == 1) {
- if ((((float) timer * frameRate) / rippleDuration) > 0.6f)
- paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timerEmpty * frameRate) / (durationEmpty)))));
- else
- paint.setAlpha(rippleAlpha);
- } else
- paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timer * frameRate) / rippleDuration))));
- }
- timer += offect;
- }
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- WIDTH = w;
- HEIGHT = h;
- scaleAnimation = new ScaleAnimation(1.0f, zoomScale, 1.0f, zoomScale, w / 2, h / 2);
- scaleAnimation.setDuration(zoomDuration);
- scaleAnimation.setRepeatMode(Animation.REVERSE);
- scaleAnimation.setRepeatCount(1);
- }
- /**
- * Launch Ripple animation for the current view with a MotionEvent
- *
- * @param event MotionEvent registered by the Ripple gesture listener
- */
- public void animateRipple(MotionEvent event) {
- createAnimation(event.getX(), event.getY());
- }
- /**
- * Launch Ripple animation for the current view centered at x and y position
- *
- * @param x Horizontal position of the ripple center
- * @param y Vertical position of the ripple center
- */
- public void animateRipple(final float x, final float y) {
- createAnimation(x, y);
- }
- /**
- * Create Ripple animation centered at x, y
- *
- * @param x Horizontal position of the ripple center
- * @param y Vertical position of the ripple center
- */
- private void createAnimation(final float x, final float y) {
- if (this.isEnabled() && !animationRunning) {
- if (hasToZoom)
- this.startAnimation(scaleAnimation);
- radiusMax = Math.max(WIDTH, HEIGHT);
- if (rippleType != 2)
- radiusMax /= 2;
- radiusMax -= ripplePadding;
- if (isCentered || rippleType == 1) {
- this.x = getMeasuredWidth() / 2;
- this.y = getMeasuredHeight() / 2;
- } else {
- this.x = x;
- this.y = y;
- }
- animationRunning = true;
- if (rippleType == 1 && originBitmap == null)
- originBitmap = getDrawingCache(true);
- invalidate();
- }
- }
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (gestureDetector.onTouchEvent(event)) {
- animateRipple(event);
- sendClickEvent(false);
- rippleStatus = RIPPLE_SINGLE;
- }
- if (rippleStatus == RIPPLE_LONG_PRESS) {
- if (event.getAction() == MotionEvent.ACTION_UP) {
- rippleStatus = RIPPLE_ACTION_UP;
- } else if (Math.abs(event.getX() - lastLongPressX) >= touchSlop ||
- Math.abs(event.getY() - lastLongPressY) >= touchSlop) {
- rippleStatus = RIPPLE_ACTION_MOVE;
- }
- }
- return super.onTouchEvent(event);
- }
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- this.onTouchEvent(event);
- return super.onInterceptTouchEvent(event);
- }
- /**
- * Send a click event if parent view is a Listview instance
- *
- * @param isLongClick Is the event a long click ?
- */
- private void sendClickEvent(final Boolean isLongClick) {
- if (getParent() instanceof AdapterView) {
- final AdapterView adapterView = (AdapterView) getParent();
- final int position = adapterView.getPositionForView(this);
- final long id = adapterView.getItemIdAtPosition(position);
- if (isLongClick) {
- if (adapterView.getOnItemLongClickListener() != null)
- adapterView.getOnItemLongClickListener().onItemLongClick(adapterView, this, position, id);
- } else {
- if (adapterView.getOnItemClickListener() != null)
- adapterView.getOnItemClickListener().onItemClick(adapterView, this, position, id);
- }
- }
- }
- private Bitmap getCircleBitmap(final int radius) {
- final Bitmap output = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(output);
- final Paint paint = new Paint();
- final Rect rect = new Rect((int) (x - radius), (int) (y - radius), (int) (x + radius), (int) (y + radius));
- paint.setAntiAlias(true);
- canvas.drawARGB(0, 0, 0, 0);
- canvas.drawCircle(x, y, radius, paint);
- paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
- canvas.drawBitmap(originBitmap, rect, rect, paint);
- return output;
- }
- /**
- * Set Ripple color, default is #FFFFFF
- *
- * @param rippleColor New color resource
- */
- public void setRippleColor(@ColorRes int rippleColor) {
- this.rippleColor = ContextCompat.getColor(getContext(), rippleColor);
- }
- public int getRippleColor() {
- return rippleColor;
- }
- public RippleType getRippleType() {
- return RippleType.values()[rippleType];
- }
- /**
- * Set Ripple type, default is RippleType.SIMPLE
- *
- * @param rippleType New Ripple type for next animation
- */
- public void setRippleType(final RippleType rippleType) {
- this.rippleType = rippleType.ordinal();
- }
- public Boolean isCentered() {
- return isCentered;
- }
- /**
- * Set if ripple animation has to be centered in its parent view or not, default is False
- *
- * @param isCentered
- */
- public void setCentered(final Boolean isCentered) {
- this.isCentered = isCentered;
- }
- public int getRipplePadding() {
- return ripplePadding;
- }
- /**
- * Set Ripple padding if you want to avoid some graphic glitch
- *
- * @param ripplePadding New Ripple padding in pixel, default is 0px
- */
- public void setRipplePadding(int ripplePadding) {
- this.ripplePadding = ripplePadding;
- }
- public Boolean isZooming() {
- return hasToZoom;
- }
- /**
- * At the end of Ripple effect, the child views has to zoom
- *
- * @param hasToZoom Do the child views have to zoom ? default is False
- */
- public void setZooming(Boolean hasToZoom) {
- this.hasToZoom = hasToZoom;
- }
- public float getZoomScale() {
- return zoomScale;
- }
- /**
- * Scale of the end animation
- *
- * @param zoomScale Value of scale animation, default is 1.03f
- */
- public void setZoomScale(float zoomScale) {
- this.zoomScale = zoomScale;
- }
- public int getZoomDuration() {
- return zoomDuration;
- }
- /**
- * Duration of the ending animation in ms
- *
- * @param zoomDuration Duration, default is 200ms
- */
- public void setZoomDuration(int zoomDuration) {
- this.zoomDuration = zoomDuration;
- }
- public int getRippleDuration() {
- return rippleDuration;
- }
- /**
- * Duration of the Ripple animation in ms
- *
- * @param rippleDuration Duration, default is 400ms
- */
- public void setRippleDuration(int rippleDuration) {
- this.rippleDuration = rippleDuration;
- }
- public int getFrameRate() {
- return frameRate;
- }
- /**
- * Set framerate for Ripple animation
- *
- * @param frameRate New framerate value, default is 10
- */
- public void setFrameRate(int frameRate) {
- this.frameRate = frameRate;
- }
- public int getRippleAlpha() {
- return rippleAlpha;
- }
- /**
- * Set alpha for ripple effect color
- *
- * @param rippleAlpha Alpha value between 0 and 255, default is 90
- */
- public void setRippleAlpha(int rippleAlpha) {
- this.rippleAlpha = rippleAlpha;
- }
- public void setOnRippleCompleteListener(OnRippleCompleteListener listener) {
- this.onCompletionListener = listener;
- }
- /**
- * Defines a callback called at the end of the Ripple effect
- */
- public interface OnRippleCompleteListener {
- void onComplete(RippleView rippleView);
- }
- public enum RippleType {
- SIMPLE(0),
- DOUBLE(1),
- RECTANGLE(2);
- int type;
- RippleType(int type) {
- this.type = type;
- }
- }
- }
使用也很简单,把RippleView作为最外层布局就行了。
- <?xml version="1.0" encoding="utf-8"?>
- <com.dl7.listclickfeedback.RippleView
- android:id="@+id/item_ripple"
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:rv_listMode="true"
- app:rv_color="#88ff5722"
- app:rv_rippleDuration="800"
- app:rv_type="rectangle">
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
- <ImageView
- android:id="@+id/iv_icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:layout_margin="30dp"
- android:src="@mipmap/ic_face_funny"/>
- <TextView
- android:id="@+id/tv_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:gravity="center"/>
- </LinearLayout>
- </com.dl7.listclickfeedback.RippleView>
最后是点击事件的处理,需要让RippleView来接收处理点击事件。
- @Override
- public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
- // ......
- ((ViewHolder) holder).mItemRipple.setOnRippleCompleteListener(new RippleView.OnRippleCompleteListener() {
- @Override
- public void onComplete(RippleView rippleView) {
- ToastUtils.showToast(mDatas.get(position));
- }
- });
- }
这样就OK了,最后贴点图来说明反馈效果。
背景色反馈:
前景色反馈:
波纹反馈:
例子代码:列表点击反馈
- ListView和RecyclerView列表点击反馈
- ListView和RecyclerView列表点击反馈
- ListView和RecyclerView点击返回顶部
- 聊天对话反馈到ListView,点击读取
- 监听ListView列表项点击和滚动事件
- ListView点击事件冲突和删除一条数据刷新列表
- Android ListView ArrayAdapter同时实现列表和按钮点击事件
- ListView和RecyclerView比较
- RecyclerView 和 ListView 区别
- 浅谈ListView和RecyclerView
- 切换listview和RecyclerView
- RecyclerView与ListView点击事件的区别
- RecyclerView嵌套ListView解决点击事件问题
- RecyclerView、ListView实现单选列表
- recyclerView的接口回调点击事件和多级列表展示
- (更新版)Android VideoPlayer 在滚动列表实现item视频播放(ListView控件和RecyclerView)
- ListView 和RecyclerView的比较
- RecyclerView-干掉Listview和GridView
- MVP+Dagger2+Rxjava+Retrofit+GreenDao 开发的小应用,包含新闻、图片、视频3个大模块,代码封装良好
- pat-basic-1042-c语言
- CCF WC2017 & THU WC2017 旅游记
- 让 sudo 会话时间随心所欲
- 浅谈android6.0的新特性
- ListView和RecyclerView列表点击反馈
- 初学Android:内容提供器小结
- 嵌入式软件工程师岗位需求
- SeamSeg: Video Object Segmentation using Patch Seams
- Matlab中处理多个图像的索引下标
- 面向过程编程VS面向对象编程
- 解决Delphi ADO无法为更新定位行。一些值可能已在最后一次读取后已更改的问题
- dubbo基于spi扩展
- java系列笔记---异常