自定义 View(一)仿 QQ 列表 Item 侧拉删除功能
博主聲明:
轉(zhuǎn)載請在開頭附加本文鏈接及作者信息,并標記為轉(zhuǎn)載。本文由博主?威威喵?原創(chuàng),請多支持與指教。
本文首發(fā)于此? ?博主:威威喵??|??博客主頁:https://blog.csdn.net/smile_running
?
系列文章:
自定義 View(一)仿 QQ 列表 Item 側(cè)拉刪除功能
自定義 View(二)自己動手實現(xiàn)下拉刷新、上拉加載功能
自定義 View(三)仿 DrawerLayout 實現(xiàn)側(cè)拉功能
? ?分享我學習過的一個ListView側(cè)滑的Item菜單效果,所謂舉一反三,一同百通。在我學會了這個之后,很容易的就實現(xiàn)ListView的上拉加載和下拉刷新的效果,還有我們最常見的側(cè)滑抽屜效果。百變不離其中,只要我們搞懂其中的一個實現(xiàn)方式,那么其他的便信手拈來。
? ? 目的是為了,解決ListView與側(cè)滑刪除按鈕的滑動沖突
? ? 我要實現(xiàn)的是這樣一個效果,QQ的聯(lián)系人側(cè)滑菜單,而在沒學過自定義View之前毫無頭緒。既然這樣,我先來看一下實現(xiàn)后的效果吧。
完成的效果圖-  Item布局
? ? 現(xiàn)在呢,我們得有一個這樣的思路。所謂ListView中Item布局,我們一般是這個樣子的:一個父容器,里面一個TextView,ImageView等等內(nèi)容。比如:
<listview.example.x.slidelistview.SlideLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="100dp"android:orientation="horizontal"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="horizontal"android:padding="2dp"><de.hdodenhof.circleimageview.CircleImageViewandroid:id="@+id/item_image"android:layout_width="100dp"android:layout_height="match_parent"android:padding="4dp" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:id="@+id/item_name"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"android:gravity="center_vertical"android:paddingLeft="8dp"android:textColor="#323232"android:textSize="25sp" /><TextViewandroid:id="@+id/item_message"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"android:gravity="center_vertical"android:paddingLeft="8dp"android:textColor="#cfcfcf"android:textSize="18sp" /></LinearLayout></LinearLayout><LinearLayoutandroid:layout_width="200dp"android:layout_height="match_parent"android:orientation="horizontal"><TextViewandroid:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:background="@android:color/darker_gray"android:gravity="center"android:text="置頂"android:textColor="@android:color/white"android:textSize="22sp" /><TextViewandroid:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:background="@android:color/holo_red_light"android:gravity="center"android:text="刪除"android:textColor="@android:color/white"android:textSize="22sp" /></LinearLayout> </listview.example.x.slidelistview.SlideLayout>? ? 以上的ListView的Item布局,也就是本次我們使用的。
? ? 既然,你要學習側(cè)滑的實現(xiàn)效果,別告訴我,你還不會用ListView。當然也是有可能的,那么在這里給一篇ListView的使用姿勢文章,不會的請點擊這里:ListView使用技巧、優(yōu)化和用法拓展,掌握ListView
-  自定義存放Item父容器
? ? 首先,我們存放Item的父容器,這個父容器是有文章的。怎么說呢,大家可以看到上面布局代碼中的父容器,其實這就是我自定義的一個繼承FrameLayout的類,當然也可以是RelativeLayout。但是別用LinerLayout,因為我們要對子視圖進行排列位置,用LinerLayout反正適得其反。
? ? 有了這個前提,我們的思路就是:通過onLayout()、onMesure()方法對子視圖的測量寬高以及布置它的位置。我們的繼承自FrameLayout的類SlideLayout,其中存放兩個父容器。一個是ContentView,一個是MenuView。因為MenuView是從右側(cè)滑進來的,我們應該將它布置到屏幕以外,通過滑動顯示和隱藏。
? ? 我特地畫了一張草圖,以便更好的理解吧,也許只有我才能看懂~哈哈哈哈
menuView的邏輯理解圖,從隱藏到顯示整個過程??????
? ? 好好理解一下吧,好記性不過爛筆頭,在本子上多涂涂畫畫,或者開啟畫板自己畫一畫,才能更好的理解。
? ? 那么,我做這些有什么用呢?當然了,請看效果圖:
- 效果圖
? ? 我們來看看實現(xiàn)的代碼類:
/*** @Created by xww.* @Creation time 2018/8/21.*/public class SlideLayout extends FrameLayout {private View mContentView;private View mMenuView;private int mMenuWidth;private int mMenuHeight;private int mContentWidth;private int mContentHeight;private Scroller mScroller;private float startX;private float startY;public SlideLayout(@NonNull Context context, @Nullable AttributeSet attrs) {super(context, attrs);mScroller = new Scroller(context);}@Overrideprotected void onFinishInflate() {super.onFinishInflate();mContentView = getChildAt(0);mMenuView = getChildAt(1);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);mContentWidth = getMeasuredWidth();mContentHeight = getMeasuredHeight();mMenuWidth = mMenuView.getMeasuredWidth();mMenuHeight = mMenuView.getMeasuredHeight();}@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);//將menu布局到右側(cè)不可見(屏幕外)mMenuView.layout(mContentWidth, 0, mContentWidth + mMenuWidth, mMenuHeight);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);}@Overridepublic boolean onTouchEvent(MotionEvent event) {final float x = event.getX();final float y = event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:startX = x;startY = y;break;case MotionEvent.ACTION_MOVE:final float dx = (int) (x - startX);final float dy = (int) (startY - y);int disX = (int) (getScrollX() - dx);if (disX <= 0) {disX = 0;} else if (disX >= mMenuWidth) {disX = mMenuWidth;}scrollTo(disX, getScrollY());startX = x;startY = y;break;case MotionEvent.ACTION_UP:if (getScrollX() < mMenuWidth / 2) {closeMenu();} else {openMenu();}break;}return true;}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {return super.onInterceptTouchEvent(ev);}@Overridepublic void computeScroll() {super.computeScroll();if (mScroller.computeScrollOffset()) {scrollTo(mScroller.getCurrX(), mScroller.getCurrY());invalidate();}}public final void openMenu() {mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);invalidate();}public final void closeMenu() {mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);invalidate();} }-  Bug分析
? ? 以上只是簡單的實現(xiàn)了而且,但是明顯有很多bug,比如每個都可以滑出來、滑到一半卡主、與ListView滑動沖突了等等,下面我們一點一點來解決它,為了更好的體驗。
·一、解決滑動沖突
? ? 大家可能有疑惑,這個滑動沖突是怎么產(chǎn)生的呢?下面,我們來看一下滑動沖突的結(jié)果,你就會明白它產(chǎn)生的大致原因了
滑動沖突了? ? 分析一下,我們SlideLayout有左右滑動的動作,ListView有上下滑動的動作。當我在左右滑動的時候不松開而進行上下滑動,那么滑動事件便由SlideLayout傳遞到了ListView的滑動。可想而知,SlideLayout的滑動事件被父視圖給奪走了,所以SlideLayout就犯毛病了,停止了它的滑動行為。
? ? 既然是ListView奪走了touch事件,從ListView角度上來說,我(ListView)攔截了SlideLayout的touch事件;從SlideLayout的角度上來說,父(ListView)把我的touch事件給奪走了。
? ? 那么有兩種相對的角度,就有兩種解決的方法。第一種是繼承ListView,設置不攔截;第二種是剝奪ListView對touch的處理權(quán)。我就用第二種來實現(xiàn),既然你可以奪走我的,那么我就可以拿回來。所謂相生相克,一物降一物。
? ? 我們思路應該這樣,比如手指在左右滑動的距離大于上下滑動的距離,那么判定是SlideLayout的滑動事件,在這種情況下才去剝奪ListView的事件處理器。
看一下我們實現(xiàn)的代碼:在onTouchEvent();的ACTION_MOVE事件中修改代碼,添加如下部分
case MotionEvent.ACTION_MOVE:final float dx = (int) (x - startX);final float dy = (int) (startY - y);int disX = (int) (getScrollX() - dx);if (disX <= 0) {disX = 0;} else if (disX >= mMenuWidth) {disX = mMenuWidth;}scrollTo(disX, getScrollY());final float moveX = Math.abs(x - downX);final float moveY = Math.abs(y - downY);if (moveX > moveY && moveX > 10f) {//剝奪ListView對touch事件的處理權(quán)getParent().requestDisallowInterceptTouchEvent(true);}startX = x;startY = y;break;? ? 通過以上的修改,我們達到了目的,看看效果吧。
解決滑動沖突bug二、解決Item點擊事件的沖突
? ? 我們的Item內(nèi)可能是有點擊事件的,所以呢,又掉入了一個坑。為什么這樣說呢?看下面的圖你就明白一切了。我給名字設置了點擊事件,發(fā)生了什么?
在姓名區(qū)域無法滑動,其他區(qū)域可以滑動? ? 分析一下,產(chǎn)生這種異常的原因。為什么在姓名那里卻無法移動呢?因為什么呢?我們就知道了onClickListener其實把touch事件給消費了,這就導致了SlideLayout始終處理不了touch事件,因為被Item中的子元素TextView給消費了。
? ? 如果不太懂,你也許可以先看這樣的一個簡單例子:理解View的事件分發(fā)、攔截和消費,處理事件沖突的必備技能
? ? 既然,TextView是它(SlideLayout)的兒子,作為父親,那肯定要管管啊,不能讓兒子亂來吧。那么,下面我們就管一管,怎么管呢?當然是攔截了,父親把touch事件給攔截下來。但是,總不能所有情況都攔截吧,那兒子不就廢了嗎?所以呢,我們給定這樣一個條件,就是當手指確實在左右滑動,而非單純的點擊時,這時父親才攔截兒子的touch事件。也許有點難以想象,不急,我們看一下代碼就簡單多了。
@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {boolean intercept = false;final float x = event.getX();final float y = event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:downX = x;downY = y;break;case MotionEvent.ACTION_MOVE:final float moveX = Math.abs(x - downX);if (moveX > 10f) {//對兒子touch事件進行攔截intercept = true;}break;case MotionEvent.ACTION_UP:break;}return intercept;}? ? 通過這樣,我們的兒子就聽話多了,那么來看看是這樣的一種效果。
解決點擊事件與滑動的bug? ? 這一路,我的bug層出不窮,解決了這個又出了新的bug,真的是一步一個坑。當然,這是幸運的事情,當你爬完了這些坑之后,給你帶來的確實另一番天地。不禁扯了一下,言歸正傳,我們看看出現(xiàn)的Bug情況吧。
多個Item可以被拉出? ? 我們來分析一下出現(xiàn)這種情況的根本原因,那就是每個Item都是ListView中獨立的一個個體,我們的Count數(shù)多少也就是Item數(shù)的多少,所以我們要對每個Item的側(cè)滑Menu進行監(jiān)聽,我們自定義一個接口用于保存每一個Item的狀態(tài),有兩種:Open和Close。但我們還需要一個點擊的監(jiān)聽,用于判斷是否與當前Item一致。
? ? 這部分就比較簡單了,也好理解,簡單的看一下代碼吧。首先,在SlideLayout中定義一個監(jiān)聽接口
private onSlideChangeListenr onSlideChangeListenr;public interface onSlideChangeListenr {void onMenuOpen(SlideLayout slideLayout);void onMenuClose(SlideLayout slideLayout);void onClick(SlideLayout slideLayout);}public void setOnSlideChangeListenr(SlideLayout.onSlideChangeListenr onSlideChangeListenr) {this.onSlideChangeListenr = onSlideChangeListenr;}? ? 其次,在三個地方分別設置監(jiān)聽
@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {boolean intercept = false;final float x = event.getX();final float y = event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:downX = x;downY = y;if (onSlideChangeListenr != null) {onSlideChangeListenr.onClick(this);}break;case MotionEvent.ACTION_MOVE:final float moveX = Math.abs(x - downX);if (moveX > 10f) {//對兒子touch事件進行攔截intercept = true;}break;case MotionEvent.ACTION_UP:break;}return intercept;}public final void openMenu() {mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);invalidate();if (onSlideChangeListenr != null) {onSlideChangeListenr.onMenuOpen(this);}}public final void closeMenu() {mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);invalidate();if (onSlideChangeListenr != null) {onSlideChangeListenr.onMenuClose(this);}}? ? 最后,在我們適配器中設置監(jiān)聽狀態(tài)的改變做出相應的處理。
mSlideLayout = (SlideLayout) convertView;mSlideLayout.setOnSlideChangeListenr(new SlideLayout.onSlideChangeListenr() {@Overridepublic void onMenuOpen(SlideLayout slideLayout) {mSlideLayout = slideLayout;}@Overridepublic void onMenuClose(SlideLayout slideLayout) {if (mSlideLayout != null) {mSlideLayout = null;}}@Overridepublic void onClick(SlideLayout slideLayout) {if (mSlideLayout != null ) {mSlideLayout.closeMenu();}}});? ? 接下來,我們運行一下項目,成功的解決了問題。
? ? 到此,一個仿QQ側(cè)滑菜單的效果完全的實現(xiàn)了,是不是很贊呢?哈哈哈哈。你以為就結(jié)束了嗎?開玩笑。。。哈哈哈哈哈哈哈!
總結(jié)
以上是生活随笔為你收集整理的自定义 View(一)仿 QQ 列表 Item 侧拉删除功能的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 万能计算机作文,万能作文(万能通用作文6
- 下一篇: App红海
