国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

自定義View 篇四《低仿QQ測拉刪除》

2019-11-09 14:38:19
字體:
來源:轉載
供稿:網友

都知道QQ有一個比較牛逼的效果就是測拉刪除效果,目前這個功能,網上自定義控件也有很多實現方式了,本篇也自己實現一個測拉刪除效果的自定義控件。雖然功能一樣,實現方式不同罷了,也希望提供一些思路,對自己和讀者有些幫助~

由于QQ測拉功能強大,手寫文字耗費時間,就做個低配置版的測拉效果。廢話不多講,還是乖乖搞事情吧~

1、實現測拉刪除的真整體布局:

對于自定義View的布局:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:id="@+id/activity_main"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context="com.itydl.a07sweepview.MainActivity">    <com.itydl.a07sweepview.SweepView        android:id="@+id/sv"        android:layout_width="match_parent"        android:layout_height="65dp"        >        <!--左側內容區域-->        <include layout="@layout/content"/>        <!--右側刪除區域-->        <include layout="@layout/delete"/>    </com.itydl.a07sweepview.SweepView></RelativeLayout>通過include的方式引入布局。這兩個布局分別表示內容區域,和左側刪除區域。代碼如下:

content.xml:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:orientation="vertical"              android:layout_width="match_parent"              android:layout_height="80dp">    <TextView        android:gravity="center"        android:textColor="#ffffff"        android:background="#88000000"        android:textSize="25sp"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:text="測試數據"/></LinearLayout>delete.xml:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:orientation="vertical"              android:layout_width="150dp"              android:layout_height="65dp">    <TextView        android:gravity="center"        android:textColor="#ffffff"        android:background="#ff0000"        android:textSize="25sp"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:text="刪除"/></LinearLayout>

自定義View的代碼:

public class SweepView extends ViewGroup {    PRivate View mContentView;    private View mDeleteView;    private int mDeleteWidth;    public SweepView(Context context) {        this(context, null);    }    public SweepView(Context context, AttributeSet attrs) {        super(context, attrs);    }    //xml文件加載完成    @Override    protected void onFinishInflate() {        //一般用于拿到孩子對象        mContentView = getChildAt(0);        mDeleteView = getChildAt(1);        //拿到DeleteView的params對象        LayoutParams params = mDeleteView.getLayoutParams();        mDeleteWidth = params.width;    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        //測量孩子        mContentView.measure(widthMeasureSpec, heightMeasureSpec);        int measureSpecWidth = MeasureSpec.makeMeasureSpec(mDeleteWidth, MeasureSpec.EXACTLY);        mDeleteView.measure(measureSpecWidth, heightMeasureSpec);        int widthMeasureSpecSelf = MeasureSpec.getSize(widthMeasureSpec);        int heightMeasureSpecSelf = MeasureSpec.getSize(heightMeasureSpec);        //設置自定義View的大小        setMeasuredDimension(widthMeasureSpecSelf, heightMeasureSpecSelf);    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        //給孩子布局        int contentWidth = mContentView.getMeasuredWidth();        int contentHeight = mContentView.getMeasuredHeight();        mContentView.layout(0, 0, contentWidth, contentHeight);        int deleteWidth = mDeleteView.getMeasuredWidth();        int deleteHeight = mDeleteView.getMeasuredWidth();        mDeleteView.layout(contentWidth, 0, contentWidth + deleteWidth, deleteHeight);    }}上面進行layout和measue相信已經簡單到跟寫button代碼一樣easy了,沒啥好說的。

運行程序:

知識簡單的布局,點擊并沒辦法滑動。接下來實現滑動效果:

這里滑動采用v4包里面的工具類:ViewDragHelper

2、ViewDragHelper在本項目中的使用:

1)、創建實例

public SweepView(Context context, AttributeSet attrs) {    super(context, attrs);    mDragHelper = ViewDragHelper.create(this,new MyCallBack());}2、touch事件委托給ViewDragHelper離開監聽處理,在它內部把觸摸事件封裝的很好了。

@Overridepublic boolean onTouchEvent(MotionEvent event) {    mDragHelper.processTouchEvent(event);    return true;}3、實現自己的callback:

我們在實例化ViewDragHelper的時候, 這里參數1就代表自定義的View,傳入this即可。著重說一下callBack,我們通過創建內部類方式,創建MyCallBack類,并重寫里面的回調方法:

class MyCallBack extends ViewDragHelper.Callback{    //是否分析(返回true才會有效)view的touch;參數1:觸摸的view;2:touch的id。    @Override    public boolean tryCaptureView(View child, int pointerId) {        // 去分析child。表示我分析ContentView和DeleteView的topuch事件        System.out.println(child == mContentView);        return child == mContentView || child == mDeleteView;    }    @Override    public int clampViewPositionHorizontal(View child, int left, int dx) {        System.out.println(left);        return left;    }    @Override    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {    }    @Override    public void onViewReleased(View releasedChild, float xvel, float yvel) {    }}這里一共重寫了四個回調方法。我們一一解釋都代表什么意思,以及每個方法功能和參數意義。

1)、tryCaptureView(View child, int pointerId)在發生touch事件的down事件的時候回調

代表我是否分析(返回true才會有效)view的touch事件;參數1:當前觸摸的view;2:touch的id。如果這個方法返回值為false,表示已不對觸摸的view進行分析。則表示我ViewDragHelper不支持滑動效果了。后期的方法也都無效。

在里面打印了一行log,我們手滑內容區域,發現此時已經能夠滑動了:

首先,可以實現滑動效果,再此時看log日志:

02-05 08:36:02.445 4596-4596/com.itydl.a07sweepview I/System.out: true發現返回值為true、因此后續的操作才得以實現。

2)clampViewPositionHorizontal(View child, int left, int dx):水平移動的回調,發生touch時間move時回調。

當touch移動后的回調  參數1:分析的是哪個孩子組件移動了;參數2:左上角的坐標,child的左側的邊距,控件移動到左邊什么位置,值會根據移動變化;參數3:增量的x(記錄相對上一次的變化量dx>0右滑,dx<0左滑)。這里的值是預期的值,可以在這里做邊距的監測,做越界處理。該方法的返回值表示:// 確定要移動多少,移動到什么位置去  return left;。【這個方法里面經常換主角,touch到哪個view這里的child就是哪個view】

這么多理論知識,估計一時明白也夠嗆,別著急,相信往下繼續學習會非常清楚的。

在這里我也做了一行打印:

02-05 08:46:59.090 4596-4596/com.itydl.a07sweepview I/System.out: 34-----3402-05 08:46:59.107 4596-4596/com.itydl.a07sweepview I/System.out: 66-----3202-05 08:46:59.125 4596-4596/com.itydl.a07sweepview I/System.out: 99-----3302-05 08:46:59.142 4596-4596/com.itydl.a07sweepview I/System.out: 146-----4702-05 08:46:59.159 4596-4596/com.itydl.a07sweepview I/System.out: 177-----3102-05 08:46:59.175 4596-4596/com.itydl.a07sweepview I/System.out: 203-----2602-05 08:46:59.200 4596-4596/com.itydl.a07sweepview I/System.out: 227-----24

通過log可以更加清晰的了解參數的具體意義,那個dx值是一個變化量。必須我最初left=0,下一次=10,第三次=60.那么dx分別為:10,50

除了clampViewPositionHorizontal當然還有clampViewPositionHorizontal,會一種相信另一種也是信手拈來。

3)、onViewPositionChanged(View changedView, int left, int top, int dx, int dy)當【控件位置】移動時的回調

參數意義:// @changedView: 哪個view移動了      // @left,top:view移動后的左上角的坐標   // @dx,dy: 移動的增量

這個方法跟上邊clampViewPositionHorizontal差不多,都是移動回調,如果說clampViewPositionHorizontal用于處理越界以及確定位置的話,那么onViewPositionChanged一般用于移動view的布局重置。后續代碼可以看到兩者的各自責任,以及實現什么功能。

4)、onViewReleased(View releasedChild, float xvel, float yvel)松開收時候的回調up事件的回調。

@releasedChild:松開了哪個view; @xvel,yvel:速率。該方法一般用于“松手回彈”效果的操作,即松開手,自定義控件往哪個位置回彈。上一篇自定義ViewPage可以看到回彈效果,那里是使用Scrollor實現的,而在ViewDragHelper有它的手段,稍后會用到。

3、滑動刪除滑動的實現。

了解了上述幾個方法,我們就要在這幾個方法里面做一些操作了,比如先實現滑動效果。不僅僅點擊contentview區域可滑動,點擊deleteview區域也可以實現控件的整體滑動效果。

代碼如下:

@Overridepublic void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {    int contentWidth = mContentView.getMeasuredWidth();    int contentHeight = mContentView.getMeasuredHeight();    int deleteWidth = mDeleteView.getMeasuredWidth();    int deleteHeight = mDeleteView.getMeasuredWidth();    if (changedView == mContentView) {        mDeleteView.layout(contentWidth + left, 0, contentWidth + deleteWidth + left, deleteHeight);    } else if (changedView == mDeleteView) {        mContentView.layout(left-contentWidth,0,left,contentHeight);    }}

就像前面介紹所說的在onViewPositionChanged方法中根據move事件,對xml可以做重新布局操作。上面代碼的值都是一些很簡單的小算法,相信看起來還是蠻簡單的。當我們滑動灰色內容區域,此時的changeView就是灰色內容區域,當滑動刪除位置,此時的changeView就代表了紅色區域。

4、滑動刪除邊界的處理,解決越界問題。

@Overridepublic int clampViewPositionHorizontal(View child, int left, int dx) {    Log.e("YDL", dx + "");    if (child == mContentView) {        if (left < 0 && left < -mDeleteView.getMeasuredWidth()) {//左滑            return -mDeleteView.getMeasuredWidth();        } else if (left > 0) {//右滑            return 0;        }    } else if (child == mDeleteView) {        if (left > mContentView.getMeasuredWidth()) {            return mContentView.getMeasuredWidth();        }else if(left < mContentView.getMeasuredWidth() - mDeleteView.getMeasuredWidth()){            return mContentView.getMeasuredWidth() - mDeleteView.getMeasuredWidth();        }    }    return left;}就像前面介紹所說的在clampViewPositionHorizontal方法中根據move事件,根據滑動不同的子View,來確定邊界值不越界。運行程序:

此時,實現了滑動效果,并解決了越界問題。

5、實現滑動“回彈”效果

此時核心的邏輯已經實現了,接下來就是處理一些細節

@Overridepublic void onViewReleased(View releasedChild, float xvel, float yvel) {    // up時的回調    // @releasedChild:松開了哪個view    // @xvel,yvel:速率    int left = mContentView.getLeft();    int contentWidth = mContentView.getMeasuredWidth();    int contentHeight = mContentView.getMeasuredHeight();    int deleteWidth = mDeleteView.getMeasuredWidth();    int deleteHeight = mDeleteView.getMeasuredWidth();    if(-left < mDeleteView.getMeasuredWidth()/2){        mContentView.layout(0, 0, contentWidth, contentHeight);        mDeleteView.layout(contentWidth, 0, contentWidth + deleteWidth, deleteHeight);    }else{        mContentView.layout(-deleteWidth, 0, contentWidth - deleteWidth, contentHeight);        mDeleteView.layout(contentWidth - deleteWidth, 0, contentWidth, deleteHeight);    }}UP事件后,通過判斷ContentView左側坐標位置,重新確定了兩個孩子組件的位置。運行科自行調試,恢復布局很生硬,因此加入緩慢恢復功能。

修改上述代碼:

@Overridepublic void onViewReleased(View releasedChild, float xvel, float yvel) {    // up時的回調    // @releasedChild:松開了哪個view    // @xvel,yvel:速率    int left = mContentView.getLeft();    int contentWidth = mContentView.getMeasuredWidth();    int deleteWidth = mDeleteView.getMeasuredWidth();     if(-left < mDeleteView.getMeasuredWidth()/2){        mDragHelper.smoothSlideViewTo(mContentView,0,0);        mDragHelper.smoothSlideViewTo(mDeleteView,contentWidth,0);    }else{        mDragHelper.smoothSlideViewTo(mContentView,-deleteWidth,0);        mDragHelper.smoothSlideViewTo(mDeleteView,contentWidth - deleteWidth,0);    }    //效果等同于invalidate()---->會調用computeScroll    ViewCompat.postInvalidateOnAnimation(SweepView.this);}其中smoothSlideViewTo已經把平滑恢復封裝的很好了。只需要傳入View、該最終左側坐標、最終top坐標即可。

這里必須進行invalidate();刷新,使用了ViewCompat.postInvalidateOnAnimation(SweepView.this);代替,這個api可以兼容更低的版本。然而這里只是委托作用,真正的平滑移動效果在移動回調方法computeScroll()中。重寫之:

@Overridepublic void computeScroll() {    if(mDragHelper.continueSettling(true)){        //直接刷新即可        ViewCompat.postInvalidateOnAnimation(SweepView.this);    }}在這里面,只需要簡單刷新界面調用ViewCompat.postInvalidateOnAnimation(SweepView.this);即可。當我們調用ViewCompat.postInvalidateOnAnimation(SweepView.this);后,內部會調用computeScroll方法,在這里面隔一段距離并借助scrollTo()完成平滑移動。運行:

6、滑動刪除的實現:

在MainActivity中使用ListView,相信都會熟練使用。在定義Adapter適配器的時候,我們通過下面方式實現ListView加載數據,而且每個Item都加入是用自定義控件。

@Overridepublic View getView(int position, View convertView, ViewGroup parent) {    ViewHolder holder;    if (convertView == null) {        holder = new ViewHolder();        convertView = View.inflate(MainActivity.this, R.layout.item_list, null);        holder.mTextView = (TextView) convertView.findViewById(R.id.tv_content);        holder.mSweepView = (SweepView) convertView.findViewById(R.id.sv);        convertView.setTag(holder);    } else {        holder = (ViewHolder) convertView.getTag();    }    String itemStr = (String) getItem(position);    holder.mTextView.setText(itemStr);    return convertView;}getView的item的布局:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:layout_width="match_parent"              android:layout_height="match_parent"              android:orientation="vertical">    <com.itydl.a07sweepview.SweepView        android:id="@+id/sv"        android:layout_width="match_parent"        android:layout_height="65dp"        >        <!--左側內容區域-->        <include layout="@layout/content"/>        <!--右側刪除區域-->        <include layout="@layout/delete"/>    </com.itydl.a07sweepview.SweepView></LinearLayout>運行程序:

7、實現真正的側欄刪除,以及一些細節的處理。

首先添加可刪除事件,直接在getView方法里面設置item上的孩子組件的點擊事件即可。

//給listview的Item上添加點擊事件,點擊刪除該item條目holder.mTextDelet.setOnClickListener(new View.OnClickListener() {    @Override    public void onClick(View v) {        mList.remove(position);        notifyDataSetChanged();    }});運行程序:

此時已經可以完成刪除了,只不過我們發現刪除之后接著看下一條item不對勁,我沒有滑動下一條item,怎么這么顯示呢?還有我們往下滑動ListView,由于復用的原因,也會出現類似情況。

那么緊跟著解決細節問題:

要解決問題其實也挺簡單,只需要再點擊item的時候,關閉掉所有打開的item即可。那么,如何才能控制打開與關閉呢?其實在前面的恢復布局就已經隱含了這個功能。只需要把原來位置抽取一個方法就好了。

@Overridepublic void onViewReleased(View releasedChild, float xvel, float yvel) {    // up時的回調    // @releasedChild:松開了哪個view    // @xvel,yvel:速率    int left = mContentView.getLeft();    int contentWidth = mContentView.getMeasuredWidth();    int deleteWidth = mDeleteView.getMeasuredWidth();     if(-left < mDeleteView.getMeasuredWidth()/2){        mDragHelper.smoothSlideViewTo(mContentView,0,0);        mDragHelper.smoothSlideViewTo(mDeleteView,contentWidth,0);    }else{        mDragHelper.smoothSlideViewTo(mContentView,-deleteWidth,0);        mDragHelper.smoothSlideViewTo(mDeleteView,contentWidth - deleteWidth,0);    }    //效果等同于invalidate()---->會調用computeScroll    ViewCompat.postInvalidateOnAnimation(SweepView.this);}修改為:

@Overridepublic void onViewReleased(View releasedChild, float xvel, float yvel) {    // up時的回調    // @releasedChild:松開了哪個view    // @xvel,yvel:速率    int left = mContentView.getLeft();     if(-left < mDeleteView.getMeasuredWidth()/2){         //還原item(關閉)         close();     }else{         //(打開)         open();     }}打開和關閉方法就要暴露方法出去:

/** * 關閉item */public void close() {    int contentWidth = mContentView.getMeasuredWidth();    if(mSweepChangeListener != null){        mSweepChangeListener.sweepChanged(SweepView.this,false);    }    mDragHelper.smoothSlideViewTo(mContentView,0,0);    mDragHelper.smoothSlideViewTo(mDeleteView,contentWidth,0);    //效果等同于invalidate()---->會調用computeScroll.這里必須有刷新才可以    ViewCompat.postInvalidateOnAnimation(SweepView.this);}/** * 打開item */public void open() {    int contentWidth = mContentView.getMeasuredWidth();    int deleteWidth = mDeleteView.getMeasuredWidth();    if(mSweepChangeListener != null){        mSweepChangeListener.sweepChanged(SweepView.this,true);    }    mDragHelper.smoothSlideViewTo(mContentView,-deleteWidth,0);    mDragHelper.smoothSlideViewTo(mDeleteView,contentWidth - deleteWidth,0);    //效果等同于invalidate()---->會調用computeScroll.這里必須有刷新才可以    ViewCompat.postInvalidateOnAnimation(SweepView.this);}為了判斷item的view是打開還是關閉,因此在自定義View中暴露接口出去,并在每一次getView的時候通過接口會掉的方式告知當前的item是打開還是關閉的:

public void setOnSweepChangeListener(OnSweepChangeListener listener){    this.mSweepChangeListener = listener;}public interface OnSweepChangeListener{    /**     * item是自定義SweepView對象。每個ListView的item都是SweepView對象,且不同     * @param sweepView     * @param isOpend     */    void sweepChanged(SweepView sweepView,boolean isOpend);}對于接口對象調用接口方法,上邊的打開和關閉方法中已經很給出了調用。

最后是在getView方法中作如下修改:

holder.mSweepView.setOnSweepChangeListener(new SweepView.OnSweepChangeListener() {    @Override    public void sweepChanged(SweepView sweepView, boolean isOpend) {        if(isOpend){            //如果打開,將該item對應的SweepView對象則保存至集合中            if(!mSweepViews.contains(sweepView)){                mSweepViews.add(sweepView);            }        }else{            mSweepViews.remove(sweepView);        }    }});當加載每一個 item的時候,每條item都監聽當前Item的SweepView子布局是打開還是關閉,如果打開,將該item對應的SweepView對象則保存至集合中。  而當點擊了刪除按鈕的時候,我們需要如下操作:

//給listview的Item上添加點擊事件,點擊刪除該item條目holder.mTextDelet.setOnClickListener(new View.OnClickListener() {    @Override    public void onClick(View v) {        mList.remove(itemStr);        closeAll();        notifyDataSetChanged();    }});              

private void closeAll() {    //使用迭代器        /*ListIterator<SweepView> iterator = mSweepViews.listIterator();        while (iterator.hasNext()) {            SweepView view = iterator.next();            view.close();        }*/    for (SweepView sweepView : mSweepViews) {        sweepView.close();    }}此時實現了刪除,運行程序:

可能你覺得已經完成了,其實,還存在一個嚴重的bug,當我們往下滑動的時候就可以看到了,以及我們滑動多個item都能打開,顯然不符合QQ滑動刪除效果。那么最后就解決這個bug:

解決思路:當我們按下的時候,記錄按下滑動打開時候的SweepView的實例,這個實例用臨時變量保存起來。然后通過接口回調的方式,傳遞用戶下一次按下的item的SweepView實例。這次新添加的接口回調方法,在View的onTouchEvent的Action_Down的時候調用。代碼如下:

1)添加接口方法

/** * 按下時候的回調。按下,如果按下時當前的item與打開的item不一致,按下關閉掉item * @param sweepView */void sweepDown(SweepView sweepView);2)在View的onTouchEvent的Action_Down的時候調用。

@Overridepublic boolean onTouchEvent(MotionEvent event) {    if(event.getAction() == MotionEvent.ACTION_DOWN){        //按下的時候        mSweepChangeListener.sweepDown(this);    }    mDragHelper.processTouchEvent(event);    return true;}3)、記錄按下滑動打開時候的SweepView的實例,以及實現只允許一條item展示。

private SweepView mSweepView;

holder.mSweepView.setOnSweepChangeListener(new SweepView.OnSweepChangeListener() {    @Override    public void sweepChanged(SweepView sweepView, boolean isOpend) {        if (isOpend) {            mSweepView = sweepView;            //如果打開,將該item對應的SweepView對象則保存至集合中            if (!mSweepViews.contains(sweepView)) {                mSweepViews.add(sweepView);            }        } else {            mSweepViews.remove(sweepView);        }    }    @Override    public void sweepDown(SweepView sweepView) {        //如果不滑動,mSweepView為null,因此要過濾為null情況        if(mSweepView != null && mSweepView != sweepView){            mSweepView.close();        }    }});

最后運行看看地方QQ側拉刪除的效果吧:

效果還不錯,加個關注唄~

打開微信掃描下方二維碼查看更多安卓文章:

打開微信搜索公眾號    Android程序員開發指南   或者手機掃描下方二維碼 在公眾號閱讀更多Android文章。

微信公眾號圖片:


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 安庆市| 平乡县| 秦安县| 佛教| 柘城县| 霍林郭勒市| 莲花县| 寿光市| 荥阳市| 通州市| 靖江市| 将乐县| 英吉沙县| 钟山县| 广安市| 永靖县| 长垣县| 新安县| 文成县| 上饶市| 涡阳县| 丰台区| 黄梅县| 兴城市| 班玛县| 武邑县| 台前县| 广州市| 仙桃市| 济阳县| 历史| 班玛县| 茂名市| 黄平县| 康定县| 芦溪县| 南部县| 玉树县| 曲阳县| 那坡县| 宝兴县|