編輯:關於Android編程
在最近的項目中,我的ListView中item選項是長按刪除的效果(Android的通常做法長按或點擊),老板覺得這個效果不好,提出要求做類似QQ消息列表橫向滑動時刪除消息的效果,這種效果確實感覺挺爽的,但是我也沒做過,於是就在網上搜索了一番,找到了很多例子,實現了這個效果,但在這個過程中不盡如人意,遇到一些曲折,大多數例子確實實現了效果,但是也存在一些問題沒有解決,用著有問題,如item點擊無效、item有點擊事件時滑動item結束後執行了item的點擊事件、item中有按鈕時若手指按下在按鈕位置時滑動無效等等,自己查了很多終於解決了這些問題,現整理如下供大家參考,如有問題請多多包含。
本博文實現思路是在滑動item的過程中修改item的leftMargin實現的。先看下最終效果:
下面就一步步來實現吧。
ListView中的item布局有講究,最外層使用的LinearLayout,設置其orientation屬性為horizontal。然後裡面主要包含兩個視圖組件,一個是我們正常顯示時的布局,另外一個是滑動時出現的視圖如刪除按鈕。正常顯示的視圖初始的layout_width設置為match_parent,並且滑動時出現的視圖必須指定其layout_width,不要使用match_parent或wrap_parent,否則會獲取不到寬度。稍後你會明白的。看本博文完整的item布局。
需要自定義ListView,繼承於ListView。如:
public class SideslipListView extends ListView {
...
public SideslipListView(Context context) {
super(context);
}
public SideslipListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SideslipListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
...
}
首先獲取屏幕寬度。
private int mScreenWidth;//屏幕的寬度
...
// 獲取屏幕寬度
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
mScreenWidth = dm.widthPixels;
接著重寫onTouchEvent(MotionEvent ev)方法,對手指按下、移動、離開分別進行相應處理。如
@Override
public boolean onTouchEvent(MotionEvent ev) {//事件響應
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
performActionDown(ev);
break;
case MotionEvent.ACTION_MOVE:
performActionMove(ev);
break;
case MotionEvent.ACTION_UP:
performActionUp(ev);
break;
}
return super.onTouchEvent(ev);
}
當手指按下時我們需要獲取到按下時所在的item,得到正常顯示時的視圖,並設置其寬度為屏幕的寬度;另外還需得到刪除組件的寬度。如:
private int mDownX;//手指初次按下的X坐標
private int mDownY;//手指初次按下的Y坐標
private boolean isDeleteShow;//刪除組件是否顯示
private ViewGroup mPointChild;//手指按下位置的item組件
private int mDeleteWidth;//刪除組件的寬度
private LinearLayout.LayoutParams mItemLayoutParams;//手指按下時所在的item的布局參數
...
private void performActionDown(MotionEvent ev) {
mDownX = (int) ev.getX();
mDownY = (int) ev.getY();
if (isDeleteShow) {
ViewGroup tmpViewGroup = (ViewGroup) getChildAt(pointToPosition(mDownX, mDownY) - getFirstVisiblePosition());
Log.i(TAG, "*********mPointChild.equals(tmpViewGroup): " + mPointChild.equals(tmpViewGroup));
if (!mPointChild.equals(tmpViewGroup)) {
turnNormal();
}
}
//獲取當前的item
mPointChild = (ViewGroup) getChildAt(pointToPosition(mDownX, mDownY) - getFirstVisiblePosition());
mDeleteWidth = mPointChild.getChildAt(1).getLayoutParams().width;//獲取刪除組件的寬度
Log.i(TAG, "**********pointToPosition(x,y): " + pointToPosition(mDownX, mDownY)
+ ", getFirstVisiblePosition() = " + getFirstVisiblePosition()
+ ", mDeleteWidth = " + mDeleteWidth);
mItemLayoutParams = (LinearLayout.LayoutParams) mPointChild.getChildAt(0).getLayoutParams();
mItemLayoutParams.width = mScreenWidth;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
}
當手指移動時動態修改正常是顯示視圖組件的leftMargin,如果刪除按鈕沒有顯示,手指向左滑動刪除按鈕就會根據滑動的距離顯示相應的可見寬度。如:
private boolean performActionMove(MotionEvent ev) {
int nowX = (int) ev.getX();
int nowY = (int) ev.getY();
int diffX = nowX - mDownX;
if (Math.abs(diffX) > Math.abs(nowY - mDownY) && Math.abs(nowY - mDownY) < 20) {
if (!isDeleteShow && nowX < mDownX) {//刪除按鈕未顯示時向左滑
if (-diffX >= mDeleteWidth) {//如果滑動距離大於刪除組件的寬度時進行偏移的最大處理
diffX = -mDeleteWidth;
}
mItemLayoutParams.leftMargin = diffX;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
isAllowItemClick = false;
} else if (isDeleteShow && nowX > mDownX) {//刪除按鈕顯示時向右滑
if (diffX >= mDeleteWidth) {
diffX = mDeleteWidth;
}
mItemLayoutParams.leftMargin = diffX - mDeleteWidth;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
isAllowItemClick = false;
}
return true;
}
return super.onTouchEvent(ev);
}
當手指離開時,根據移動的距離判斷是否顯示刪除按鈕。如:
private void performActionUp(MotionEvent ev) {
//如果向左滑出超過隱藏的二分之一就全部顯示
if (-mItemLayoutParams.leftMargin >= mDeleteWidth / 2) {
mItemLayoutParams.leftMargin = -mDeleteWidth;
isDeleteShow = true;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
} else {
turnNormal();
}
}
到這裡基本上就實現了側滑時刪除按鈕的顯示和隱藏了。不過還有些問題。
ListView設置了點擊事件時左右滑動會觸發改事件沖突處理當ListView中的item設置了點擊事件時,我們左右滑動時就會觸發這個點擊事件,那麼如何避免呢?在自定義的ListView中設置標志isAllowItemClick,當手指按下時設置其值為isAllowItemClick = true,在手指移動的過程中,若是發生左右移動,則設置其值為isAllowItemClick = false,另外向外公布方法:
public boolean isAllowItemClick() {
return isAllowItemClick;
}
當為Listview設置點擊事件時這樣做就可以避免了。
mSideslipListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView parent, View view, int position, long id) {
if (mSideslipListView.isAllowItemClick()) {
Log.i(TAG, mDataList.get(position) + "被點擊了");
Toast.makeText(MainActivity.this, mDataList.get(position) + "被點擊了",
Toast.LENGTH_SHORT).show();
}
}
});
當item中有按鈕時手指按下在改位置時滑動無效處理
這個跟Android的事件分發機制有關系,不知道可以去仔細看看查查資料。我們可以重寫onInterceptTouchEvent(MotionEvent ev)這個方法來解決,若手指按下並且是左右移動時則最該Touch事件進行攔截。如:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {//事件攔截
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
isAllowItemClick = true;
//側滑刪除
mDownX = (int) ev.getX();
mDownY = (int) ev.getY();
mPointPosition = pointToPosition(mDownX, mDownY);
Log.i(TAG, "*******pointToPosition(mDownX, mDownY): " + mPointPosition);
if (mPointPosition != -1) {
if (isDeleteShow) {
ViewGroup tmpViewGroup = (ViewGroup) getChildAt(mPointPosition - getFirstVisiblePosition());
if (!mPointChild.equals(tmpViewGroup)) {
turnNormal();
}
}
//獲取當前的item
mPointChild = (ViewGroup) getChildAt(mPointPosition - getFirstVisiblePosition());
mDeleteWidth = mPointChild.getChildAt(1).getLayoutParams().width;
mItemLayoutParams = (LinearLayout.LayoutParams) mPointChild.getChildAt(0).getLayoutParams();
Log.i(TAG, "*********mItemLayoutParams.height: " + mItemLayoutParams.height +
", mDeleteWidth: " + mDeleteWidth);
mItemLayoutParams.width = mScreenWidth;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
}
break;
}
case MotionEvent.ACTION_MOVE: {
int nowX = (int) ev.getX();
int nowY = (int) ev.getY();
int diffX = nowX - mDownX;
Log.i(TAG, "******dp2px(4): " + dp2px(8) + ", dp2px(8): " + dp2px(8) +
", density: " + getContext().getResources().getDisplayMetrics().density);
if (Math.abs(diffX) > dp2px(4) || Math.abs(nowY - mDownY) > dp2px(4)) {
return true;//避免子布局中有點擊的控件時滑動無效
}
break;
}
}
return super.onInterceptTouchEvent(ev);
}
public float dp2px(int dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
getContext().getResources().getDisplayMetrics());
}
到此,我們的自定義ListView基本上就搞定了。最後貼上我的自定義SideslipListView的完整源碼。
package com.junkchen.toucheventtest;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.ListView;
/**
* Created by JunkChen on 2016/3/29 0029.
*/
public class SideslipListView extends ListView {
private static final String TAG = "SideslipListView";
private int mScreenWidth;//屏幕的寬度
private boolean isDeleteShow;//刪除組件是否顯示
private ViewGroup mPointChild;//手指按下位置的item組件
private int mDeleteWidth;//刪除組件的寬度
private LinearLayout.LayoutParams mItemLayoutParams;//手指按下時所在的item的布局參數
private int mDownX;//手指初次按下的X坐標
private int mDownY;//手指初次按下的Y坐標
private int mPointPosition;//手指按下位置所在的item位置
private boolean isAllowItemClick;//是否允許item點擊
public SideslipListView(Context context) {
super(context);
init(context);
}
public SideslipListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SideslipListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
// 獲取屏幕寬度
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
mScreenWidth = dm.widthPixels;
Log.i(TAG, "***********mScreenWidth: " + mScreenWidth);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {//事件攔截
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
isAllowItemClick = true;
//側滑刪除
mDownX = (int) ev.getX();
mDownY = (int) ev.getY();
mPointPosition = pointToPosition(mDownX, mDownY);
Log.i(TAG, "*******pointToPosition(mDownX, mDownY): " + mPointPosition);
if (mPointPosition != -1) {
if (isDeleteShow) {
ViewGroup tmpViewGroup = (ViewGroup) getChildAt(mPointPosition - getFirstVisiblePosition());
if (!mPointChild.equals(tmpViewGroup)) {
turnNormal();
}
}
//獲取當前的item
mPointChild = (ViewGroup) getChildAt(mPointPosition - getFirstVisiblePosition());
mDeleteWidth = mPointChild.getChildAt(1).getLayoutParams().width;
mItemLayoutParams = (LinearLayout.LayoutParams) mPointChild.getChildAt(0).getLayoutParams();
Log.i(TAG, "*********mItemLayoutParams.height: " + mItemLayoutParams.height +
", mDeleteWidth: " + mDeleteWidth);
mItemLayoutParams.width = mScreenWidth;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
}
break;
}
case MotionEvent.ACTION_MOVE: {
int nowX = (int) ev.getX();
int nowY = (int) ev.getY();
int diffX = nowX - mDownX;
Log.i(TAG, "******dp2px(4): " + dp2px(8) + ", dp2px(8): " + dp2px(8) +
", density: " + getContext().getResources().getDisplayMetrics().density);
if (Math.abs(diffX) > dp2px(4) || Math.abs(nowY - mDownY) > dp2px(4)) {
return true;//避免子布局中有點擊的控件時滑動無效
}
break;
}
}
return super.onInterceptTouchEvent(ev);
}
public float dp2px(int dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
getContext().getResources().getDisplayMetrics());
}
@Override
public boolean onTouchEvent(MotionEvent ev) {//事件響應
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
performActionDown(ev);
break;
case MotionEvent.ACTION_MOVE:
performActionMove(ev);
break;
case MotionEvent.ACTION_UP:
performActionUp(ev);
break;
}
return super.onTouchEvent(ev);
}
private void performActionDown(MotionEvent ev) {
mDownX = (int) ev.getX();
mDownY = (int) ev.getY();
if (isDeleteShow) {
ViewGroup tmpViewGroup = (ViewGroup) getChildAt(pointToPosition(mDownX, mDownY) - getFirstVisiblePosition());
Log.i(TAG, "*********mPointChild.equals(tmpViewGroup): " + mPointChild.equals(tmpViewGroup));
if (!mPointChild.equals(tmpViewGroup)) {
turnNormal();
}
}
//獲取當前的item
mPointChild = (ViewGroup) getChildAt(pointToPosition(mDownX, mDownY) - getFirstVisiblePosition());
mDeleteWidth = mPointChild.getChildAt(1).getLayoutParams().width;//獲取刪除組件的寬度
Log.i(TAG, "**********pointToPosition(x,y): " + pointToPosition(mDownX, mDownY)
+ ", getFirstVisiblePosition() = " + getFirstVisiblePosition()
+ ", mDeleteWidth = " + mDeleteWidth);
mItemLayoutParams = (LinearLayout.LayoutParams) mPointChild.getChildAt(0).getLayoutParams();
mItemLayoutParams.width = mScreenWidth;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
}
private boolean performActionMove(MotionEvent ev) {
int nowX = (int) ev.getX();
int nowY = (int) ev.getY();
int diffX = nowX - mDownX;
if (Math.abs(diffX) > Math.abs(nowY - mDownY) && Math.abs(nowY - mDownY) < 20) {
if (!isDeleteShow && nowX < mDownX) {//刪除按鈕未顯示時向左滑
if (-diffX >= mDeleteWidth) {//如果滑動距離大於刪除組件的寬度時進行偏移的最大處理
diffX = -mDeleteWidth;
}
mItemLayoutParams.leftMargin = diffX;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
isAllowItemClick = false;
} else if (isDeleteShow && nowX > mDownX) {//刪除按鈕顯示時向右滑
if (diffX >= mDeleteWidth) {
diffX = mDeleteWidth;
}
mItemLayoutParams.leftMargin = diffX - mDeleteWidth;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
isAllowItemClick = false;
}
return true;
}
return super.onTouchEvent(ev);
}
private void performActionUp(MotionEvent ev) {
//如果向左滑出超過隱藏的二分之一就全部顯示
if (-mItemLayoutParams.leftMargin >= mDeleteWidth / 2) {
mItemLayoutParams.leftMargin = -mDeleteWidth;
isDeleteShow = true;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
} else {
turnNormal();
}
}
/**
* 轉換為正常隱藏情況
*/
public void turnNormal() {
mItemLayoutParams.leftMargin = 0;
mPointChild.getChildAt(0).setLayoutParams(mItemLayoutParams);
isDeleteShow = false;
}
/**
* 是否允許Item點擊
*
* @return
*/
public boolean isAllowItemClick() {
return isAllowItemClick;
}
}
在布局中的使用。
package com.junkchen.toucheventtest;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private SideslipListView mSideslipListView;
/**
* 初始化數據
*/
private ArrayList mDataList = new ArrayList() {
{
for (int i = 0; i < 50; i++) {
add("ListView item " + i);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSideslipListView = (SideslipListView) findViewById(R.id.sideslipListView);
mSideslipListView.setAdapter(new CustomAdapter());//設置適配器
//設置item點擊事件
mSideslipListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView parent, View view, int position, long id) {
if (mSideslipListView.isAllowItemClick()) {
Log.i(TAG, mDataList.get(position) + "被點擊了");
Toast.makeText(MainActivity.this, mDataList.get(position) + "被點擊了",
Toast.LENGTH_SHORT).show();
}
}
});
//設置item長按事件
mSideslipListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView parent, View view, int position, long id) {
if (mSideslipListView.isAllowItemClick()) {
Log.i(TAG, mDataList.get(position) + "被長按了");
Toast.makeText(MainActivity.this, mDataList.get(position) + "被長按了",
Toast.LENGTH_SHORT).show();
return true;//返回true表示本次事件被消耗了,若返回
}
return false;
}
});
}
/**
* 自定義ListView適配器
*/
class CustomAdapter extends BaseAdapter {
@Override
public int getCount() {
return mDataList.size();
}
@Override
public Object getItem(int position) {
return mDataList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (null == convertView) {
convertView = View.inflate(MainActivity.this, R.layout.item, null);
viewHolder = new ViewHolder();
viewHolder.textView = (TextView) convertView.findViewById(R.id.textView);
viewHolder.txtv_delete = (TextView) convertView.findViewById(R.id.txtv_delete);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.textView.setText(mDataList.get(position));
final int pos = position;
viewHolder.txtv_delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, mDataList.get(pos) + "被刪除了",
Toast.LENGTH_SHORT).show();
mDataList.remove(pos);
notifyDataSetChanged();
mSideslipListView.turnNormal();
}
});
return convertView;
}
}
class ViewHolder {
public TextView textView;
public TextView txtv_delete;
}
}
今天給大家帶來的是僅僅使用一個TextView實現一個高仿京東、淘寶、唯品會等各種電商APP的活動倒計時。最近公司一直加班也沒來得及時間去整理,今天難得休息想把這個分享給
前言滑動沖突可以說每一個 Android 開發者都遇到過,雖然 Android 已經在如 ViewPager 這些控件內部處理了滑動沖突,但是在我們自己定義控件,或者一些
一、MVP介紹 隨著UI創建技術的功能日益增強,UI層也履行著越來越多的職責。為了更好地細分視圖(View)與模型(Model)的功能,讓View專注於處理數據
遇到的問題如下:java.lang.NullPointerException: Attempt to invoke virtual method ‘void