Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義ListView實現側滑子菜單

Android自定義ListView實現側滑子菜單

編輯:關於Android編程

慣例,先放效果圖,DEMO在最後

\

 

想當年博主剛接觸Android的時候,看到這個效果心中只有膜拜啊,如果慢慢的自己水平也上來了,就把當年的一個想法給圓滿了吧。

好了,廢話不多說,先總結總結這個效果:

 

首先是需要自定義ListView,這點是必須的,然後在ListView的onTouchEvent方法中對事件進行處理普通的Item的話,是沒辦法實現這樣側滑的,即使你塞一個HorizontalScrollView進去都不行,所以也必須自定義一個ItemView實現左右側滑由於ListView的layout_width不一定是MATCH_PARENT,也可能是定值比如300dp,這個時候我們就需要建立一種機制來保證ItemView的寬度和ListView的寬度匹配,畢竟ItemView包含了兩個View,一個是正文的ContentView,一個是菜單的MenuView。 首先我從自定義ListView開始講起,這個ListView需要完成兩件事:事件分發和高度匹配。首先來看高度匹配:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        //寬度適配,改變ItemView的寬度
        SlideItemView.Width = width;
        for(int i = 0; i < getChildCount(); i++){
            SlideItemView item = (SlideItemView) getChildAt(i);
            item.resetWidth();
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

這個方法並沒有什麼難度,得到了ListView的寬度,並且將所有在內存中的ItemView的寬度進行重設。這一步是非常必要的,上面也說了,因為你並不知道實際ListView的寬度,那麼還談什麼左右滑動。SlideItemView的resetWidth方法我們放在後面講解。這裡就大概了解一下。 然後是ListView的事件分發,這裡就比較重要了:
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        float dx = 0;
        float dy = 0;
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                mTouchX = ev.getX();
                mTouchY = ev.getY();
                mMoveX = ev.getX();
                mMoveY = ev.getY();
                mTouchPosition = pointToPosition((int)ev.getX(), (int)ev.getY());
                break;
            case MotionEvent.ACTION_MOVE:
                dx = ev.getX() - mMoveX;
                dy = ev.getY() - mMoveY;
                if(Math.abs(dx) > Math.abs(dy)){
                    //根據坐標點得到索引值
                    int position = pointToPosition((int)ev.getX(), (int)ev.getY());
                    if(mTouchPosition != ListView.INVALID_POSITION && position == mTouchPosition){
                        //得到內存中真實的Item
                        SlideItemView itemView = (SlideItemView) getChildAt(position - getFirstVisiblePosition());
                        itemView.scroll((int) dx);
                    }
                }
                mMoveX = ev.getX();
                mMoveY = ev.getY();
                break;
            case MotionEvent.ACTION_UP:
                dx = ev.getX() - mTouchX;
                dy = ev.getY() - mTouchY;
                if(Math.abs(dx) > Math.abs(dy) && Math.abs(dx) >= mTouchSlop){
                    int position = pointToPosition((int)ev.getX(), (int)ev.getY());
                    if(mTouchPosition != ListView.INVALID_POSITION && position == mTouchPosition){
                        //得到真正在內存中的Item
                        SlideItemView itemView = (SlideItemView) getChildAt(position - getFirstVisiblePosition());
                        //根據當前scrollX以及dx判斷是否顯示正文內容
                        if (itemView.shouldShowContent((int) dx)){
                            itemView.showContent();
                        }else{
                            itemView.showMenu();
                        }
                    }else if(position != mTouchPosition){
                        SlideItemView itemView = (SlideItemView) getChildAt(mTouchPosition - getFirstVisiblePosition());
                        //根據當前scrollX以及dx判斷是否顯示正文內容
                        if (itemView.shouldShowContent((int) dx)){
                            itemView.showContent();
                        }else{
                            itemView.showMenu();
                        }
                    }
                }else{
                    SlideItemView itemView = (SlideItemView) getChildAt(mTouchPosition - getFirstVisiblePosition());
                    //根據當前scrollX以及dx判斷是否顯示正文內容
                    if (itemView.shouldShowContent((int) dx)){
                        itemView.showContent();
                    }else{
                        itemView.showMenu();
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if(mTouchPosition != ListView.INVALID_POSITION){
                    SlideItemView itemView = (SlideItemView) getChildAt(mTouchPosition - getFirstVisiblePosition());
                    itemView.showContent();
                }
                break;
        }
        return super.onTouchEvent(ev);
    }

首先啊,在ACTION_DOWN中實現對坐標點的記錄,這裡需要記錄兩套坐標點,一套是表示DOWN時的坐標,一套是表示MOVE時的坐標,MOVE時的坐標初始化是ACTION_MOVE中所必須的。然後根據當前按下的點,得出ListView的position索引。這裡還有一點非常重要,不要習慣性的在ACTION_DOWN中返回true,如果這裡返回true,那麼久表示ListView將消耗掉這個事件,並且後續的MOVE事件和UP事件都只會傳遞到ListView而不會分發到子Item中,那麼子Item就無法點擊了。具體請參考【Android事件分發】。 然後是ACTION_MOVE,如果滑動時的X坐標的絕對值比Y坐標的絕對值大,才進行下一步操作。得出當前滑動坐標所對應的ListView的position索引值,如果DOWN適合的position索引值和MOVE的position索引值相等,才開始進行滑動。這裡有一個重要的概念就是getChildAt(position - getFirstVisiblePosition()),我們知道ListView是有緩存機制的,內存中不可能存在getCount()這麼多數量的View存在,內存中只存放從getFirstVisiablePosition()到getLastVisiablePosition()這麼多個Item在內存中,那麼如果我們想得到當前的position所對應內存中的Item,就需要position - getFirstVisiblePosition()得到真正的索引。具體請參考【ListView的緩存策略】。然後呢就開始滑動吧,滑動被封裝到了ItemView裡操作,這裡主要有個了解就行了。 然後是ACTION_UP,這裡除了x坐標的絕度值比y坐標的絕對值要大意外,還需要一個額外條件就是x坐標的絕對值要大於一個阈值,只有大於這個阈值,我們才認為是滑動,這個概念很重要,這個是區別點擊操作還是滑動操作的條件。滿足條件後,我們對當前的dx偏移值進行一個判斷,如果需要展示正文Content,就展示正文,如果需要展示菜單Menu,就展示菜單。當前,這個判斷操作和展示操作都封裝在了ItemView裡。如果當前的position與ACTION_DOWN時的position不同的話,我們認定此時已經劃出這個Item了,那麼我們就需要對ACTION_DOWN所對應的ItemView進行一個判斷和展示操作。如果連滑動操作的條件都不滿足,我們認定此時是在對ListView進行上下的滾動操作,則同樣對ACTION_DOWN事件對應的ItemView進行一個判斷和展示。 最後是ACTION_CANCEL,寫這個事件呢主要是為了防止滑動過程中事件被中斷後造成滑動到一半就卡在那裡。處理方法和上面一致。   接下來就展示ItemView的部分了。這個部分需要一個必要的知識點是Scroller,如果不會請參考【Android Scroller】,假設你已經了解了Scroller。那麼就可以接著往下看了。首先展示設置正文Content和菜單Menu的部分:
public void setView(SlideListView listView, int contentId, int menuId, float menuScale){
        this.listView = listView;
        this.content = View.inflate(getContext(), contentId, null);
        this.menu = View.inflate(getContext(), menuId, null);
        this.scale = menuScale;
        LayoutParams param1 = new LayoutParams(Width, LayoutParams.MATCH_PARENT);
        addView(content, param1);
        LayoutParams param2 = new LayoutParams((int) (Width * menuScale), LayoutParams.MATCH_PARENT);
        addView(menu, param2);
    }

    public View getContent(){
        return content;
    }

    public View getMenu(){
        return menu;
    }

很簡單,非常簡單,就只是將對應的layoutId實例化,然後addView而已,唯一需要注意的就是LayoutParams這裡的Width,它是個靜態變量,它的值就是外界ListView的寬度值。接下來我們來看滑動相關的代碼:
    public void showContent(){
        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -mScroller.getFinalX(), 0);
        invalidate();
    }

    public void showMenu(){
        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), menu.getWidth() - mScroller.getFinalX(), 0);
        invalidate();
    }

    public boolean shouldShowContent(int dx){
        //初始化
        if(menu.getWidth() == 0){
            resetWidth();
        }
        if(dx > 0){
            //右滑,當滑過1/4的時候開始變化
            if(mScroller.getFinalX() < menu.getWidth() * 3 / 4){
                return true;
            }else{
                return false;
            }
        }else{
            //左滑,當滑過1/4的時候開始變化
            if(mScroller.getFinalX() < menu.getWidth() / 4){
                return true;
            }else{
                return false;
            }
        }
    }

首先看shouldShowContent方法,這裡有一個初始化操作,我們待會再講。如果dx大於0,則說明是往右滑動,那麼scrollX的值只要小於menu寬度的3/4,也就是滑動超過了1/4,我們就認為需要顯示正文Content了,否則就顯示菜單Menu。對於dx小於0的情況也同理。然後是showContent和showMenu方法,這裡直接是將scrollX滾動到兩者的起始位置,也就是說這兩個方法是在ListView的ACTION_UP方法中調用的。這裡需要注意的是invalidate()方法一定要調用。否則可能會不刷新,別問我是怎麼知道的。。我為了知道這個原因花了一個小時。。   然後是ListView中的ACTION_MOVE方法所需要調用的scroll方法:
public void scroll(int dx){
        if(dx > 0){
            //右滑
            if(mScroller.getFinalX() > 0){
                if(dx > mScroller.getFinalX()){
                    mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -mScroller.getFinalX(), 0);
                }else{
                    mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -dx, 0);
                }
            }else{
                mScroller.setFinalX(0);
            }
            invalidate();
        }else{
            //左滑
            if(mScroller.getFinalX() < menu.getWidth()){
                if(mScroller.getFinalX() - dx > menu.getWidth()){
                    mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), menu.getWidth()- mScroller.getFinalX(), 0);
                }else{
                    mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -dx, 0);
                }
            }else{
                mScroller.setFinalX(menu.getWidth());
            }
            invalidate();
        }
    }

這裡主要是有一個左右邊界值的判斷問題,大家直接看代碼吧。文字說不清楚的。邏輯其實也並不困難。 最後就是resetWidth方法了:
    /**
     * 重設寬度,在ListView的onMeasure方法中調用。
     * 此方法是為了動態適配ListView的寬度,因為ListView的layout_width不一定等於MATCH_PARENT
     * 也可能是定值比如300dp
     */
    public void resetWidth(){
        ViewGroup.LayoutParams param1 = content.getLayoutParams();
        if(param1 == null){
            param1 = new LayoutParams(Width, LayoutParams.MATCH_PARENT);
        }else{
            param1.width = Width;
        }
        content.setLayoutParams(param1);
        ViewGroup.LayoutParams param2 = menu.getLayoutParams();
        if(param2 == null){
            param2 = new LayoutParams((int) (Width * scale), LayoutParams.MATCH_PARENT);
        }else{
            param2.width = (int) (Width * scale);
        }
        menu.setLayoutParams(param2);
    }

其實也沒什麼,也就是重新改變正文Content和菜單Menu的寬度值罷了。   好了,源碼講解完畢,下面給出測試例子: 首先是activity_main.xml:



    


接著是MainActivity:
public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        SlideListView listView = (SlideListView) findViewById(R.id.listView);
        listView.setAdapter(new SlideAdapter(this, listView));
    }

}

然後是自定義Adapter,這裡需要好好看一下,特別是getView和ViewHolder的處理:
public class SlideAdapter extends BaseAdapter {

    private Context context;
    private SlideListView listView;

    public SlideAdapter(Context context, SlideListView listView){
        this.context = context;
        this.listView = listView;
    }

    private String[] data = new String[]{
            "1231231","232131231","1231231","232131231","1231231","232131231","1231231","232131231","1231231","232131231","1231231","232131231","1231231","232131231"
    };

    @Override
    public int getCount() {
        return data.length;
    }

    @Override
    public Object getItem(int position) {
        return data[position];
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if(convertView == null){
            SlideItemView itemView = new SlideItemView(context);
            itemView.setView(listView, R.layout.item_content, R.layout.item_menu, 2.0f / 3);
            holder = new ViewHolder(itemView);
            itemView.setTag(holder);
            convertView = itemView;
        }else{
            holder = (ViewHolder) convertView.getTag();
        }
        holder.textView.setText(data[position]);
        final SlideItemView itemView = (SlideItemView) convertView;
        holder.imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(context, "點擊了imageview", Toast.LENGTH_SHORT).show();
                itemView.showContent();
            }
        });
        holder.textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(context, "點擊了textview", Toast.LENGTH_SHORT).show();
                itemView.showContent();
            }
        });
        holder.btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(context, "點擊了btn1", Toast.LENGTH_SHORT).show();
                itemView.showContent();
            }
        });
        holder.btn2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(context, "點擊了btn2", Toast.LENGTH_SHORT).show();
                itemView.showContent();
            }
        });
        return convertView;
    }

    public class ViewHolder {
        public ImageView imageView;
        public TextView textView;
        public TextView btn1;
        public TextView btn2;

        public ViewHolder(SlideItemView view){
            View content = view.getContent();
            imageView = (ImageView) content.findViewById(R.id.imageView);
            textView = (TextView) content.findViewById(R.id.textView);
            View menu = view.getMenu();
            btn1 = (TextView) menu.findViewById(R.id.btn1);
            btn2 = (TextView) menu.findViewById(R.id.btn2);
        }
    }
}

最後就是兩個布局文件了 item_content.xml:



    

    
  item_menu.xml:



    

    
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved