Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義LinearLayout實現左右側滑菜單,完美兼容ListView、ScrollView、ViewPager等滑動控件

Android自定義LinearLayout實現左右側滑菜單,完美兼容ListView、ScrollView、ViewPager等滑動控件

編輯:關於Android編程

國際慣例,先來效果圖

\

 

在閱讀本文章之前,請確定熟悉【Scroller】相關的知識,如果不熟悉,請小伙伴兒先百度後再來吧。

假如你已經知道【Scroller】了,那麼就接著往下看吧。

首先,我們把側拉菜單的構造給解析出來。多次觀看上面的效果圖,我們可以得出以下的結論。

 

整體可以看做是一個ViewGroup,這個ViewGroup包含了最多三個子View(分別是左菜單的紅色View、中間正文內容的白色View、右菜單的藍色View);三個子View(我稱為UI界面,因為代碼中的Java類就取名這個)的移動是在ViewGroup的onTouchEvent方法中控制;每個UI界面都擁有獨特的東西,比如子控件布局,因此我們希望用R.layout.*的方式引入;每個UI界面又都擁有相同的屬性,比如都有寬度屬性,滑動臨界值屬性,那麼就可以用一個超類來封裝所有相似的東西;最最重要的地方,動態計算出scrollX的值,然後用Scroller來滑動。 理清楚了結構後,我們來開始第一步的設計,也就是封裝超類,首先給出代碼:
/**
 * Created by ccwxf on 2016/6/14.
 */
public abstract class UI {

    protected Context context;
    //當前UI界面的布局文件
    protected View contentView;
    //當前UI界面在父控件的起點X坐標
    protected int startX;
    //當前UI界面在父控件的終點X坐標
    protected int stopX;
    //當前UI界面的寬度
    protected int width;

    protected UI(Context context, View contentView){
        this.context = context;
        this.contentView = contentView;
    }

    protected abstract void calculate(float leftScale, float rightScale);

    protected void show(Scroller mScroller){
        if(mScroller != null){
            mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), startX - mScroller.getFinalX(), 0);
        }
    }
}
  這個UI超類就用於模擬每一個界面,其中主要封裝了內容View的設置、跳轉界面的邏輯代碼,以及暴露出去需要子類實現的calculate方法,這個calculate方法主要是要計算startX、stopX、width以及各子類獨有的屬性。  
接下來展示左菜單的實現類LeftMenuUI:
/**
 * Created by ccwxf on 2016/6/14.
 */
public class LeftMenuUI extends UI {
    // 是指要打開該UI界面所需要滾動的X坐標臨界值
    public int openX;
    // 是指要關閉該UI界面所需要的滾動的X坐標臨界值
    public int closeX;

    public LeftMenuUI(Context context, View contentView) {
        super(context, contentView);
    }

    @Override
    protected void calculate(float leftScale, float rightScale) {
        startX = 0;
        stopX = (int) (Util.getScreenWidth(context) * leftScale);
        this.width = stopX - startX;
        this.openX = (int) (startX + (1 - SideLayout.DEFAULT_SIDE) * this.width);
        this.closeX = (int) (startX + SideLayout.DEFAULT_SIDE * this.width);
    }

}

代碼那是相當的簡潔,在calculate方法中除了計算startX和stopX和width之外,還計算了openX和closeX的值。那麼問題來了,此處的openX和closeX的什麼東西呢?先看下圖所示。 \   首先黑色框代表的是整個的布局,被分為了三個部分,分別是左菜單、正文內容、右菜單。紅色框代表的是手機的屏幕,默認手機屏幕的寬高和正文內容的寬高都是一樣的。因此圖上所示是重合的。 那麼問題來了,途中所示的綠色橫線代表的openX和closeX分別是什麼意思呢?我們假想一下,我們現在正處於正文的內容,此時手指向右滑屏,將滑出左菜單的部分,此時紅框代表的屏幕就會向左移動(如果聽不懂就真的需要先了解Scroller的使用喲),如果紅色框移動到openX這個綠線的左邊,我們就認為超出了滑動的臨界值,判斷為顯示左菜單的操作,現在應該明白了openX的意思了吧,就是超過這個值就顯示左菜單。 那麼問題又來了,closeX怎麼解釋呢?我們再次假象一下,我們現在正處於左菜單,此時我們向左滑動屏幕,如果紅色框從0開始向右移動,如果超出了closeX這個臨界值,就代表我們要滑出左菜單進入正文內容,這就是closeX的意思。   好了,理解了左菜單這個類,那麼正文內容和右菜單也同樣好理解了。接下來給出正文類:ContentUI
public class ContentUI extends UI {

    public ContentUI(Context context, View contentView) {
        super(context, contentView);
    }

    @Override
    protected void calculate(float leftScale, float rightScale) {
        int width = Util.getScreenWidth(context);
        int leftWidth = (int) (width * leftScale);
        startX = leftWidth;
        stopX = leftWidth + width;
        this.width = stopX - startX;
    }

}

正文菜單更簡單,沒有openX和closeX的計算,為什麼呢?因為左菜單和右菜單的劃入劃出判斷我都放在對應的UI類裡面。接下來是RightMenuUI
/**
 * Created by ccwxf on 2016/6/14.
 */
public class RightMenuUI extends UI {
    // 是指要打開該UI界面所需要滾動的X坐標臨界值
    public int openX;
    // 是指要關閉該UI界面所需要的滾動的X坐標臨界值
    public int closeX;

    public RightMenuUI(Context context, View contentView) {
        super(context, contentView);
    }

    @Override
    protected void calculate(float leftScale, float rightScale) {
        int width = Util.getScreenWidth(context);
        startX = (int) (width * (1 + leftScale));
        stopX = (int) (width * (1 + leftScale + rightScale));
        this.width = stopX - startX;
        this.openX = (int) (startX - width + SideLayout.DEFAULT_SIDE * this.width);
        this.closeX = (int) (startX - width + (1 - SideLayout.DEFAULT_SIDE) * this.width);
    }

    /**
     * 必須重載父類方法,因為滑動的起點是從0開始
     */
    protected void show(Scroller mScroller, int measureWidth){
        if(mScroller != null){
            mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), measureWidth - Util.getScreenWidth(context) - mScroller.getFinalX(), 0);
        }
    }
}

這個類也同樣有openX和closeX的計算,但是大家要特別注意的一點是:右菜單的openX和closeX是在正文菜單的坐標內,要問為什麼的話,大家需要了解Scroller的原理並向我一樣畫一個草圖來理解。   最後就是SideLayout這個自定義控件了,其實就只是在onTouchEvent中做了滑動的邏輯判斷操作。首先給出源代碼:
/**
 * Created by ccwxf on 2016/6/14.
 */
public class SideLayout extends LinearLayout {
    //默認的菜單寬度與屏幕寬度的比值
    public static final float DEFAULT_SCALE = 0.66f;
    //默認的滑動切換閥值相對於菜單寬度的比值
    public static final float DEFAULT_SIDE = 0.25f;
    private Scroller mScroller;
    //三個UI界面
    private LeftMenuUI leftMenuUI;
    private ContentUI contentUI;
    private RightMenuUI rightMenuUI;
    //左菜單和右菜單相對於屏幕的比值
    private float leftScale = 0;
    private float rightScale = 0;
    //控件的測量寬度
    private float measureWidth = 0;
    //手指Touch時的X坐標和移動時的X坐標
    private float mTouchX;
    private float mMoveX;

    public SideLayout(Context context) {
        super(context);
        init();
    }

    public SideLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public SideLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        mScroller = new Scroller(getContext());
        setOrientation(LinearLayout.HORIZONTAL);
    }

    /**
     *  設置左菜單的布局
     * @param view 左菜單布局
     * @return 返回當類
     */
    public SideLayout setLeftMenuView(View view){
        return setLeftMenuView(view, DEFAULT_SCALE);
    }

    public SideLayout setLeftMenuView(View view, float leftScale){
        leftMenuUI = new LeftMenuUI(getContext(), view);
        this.leftScale = leftScale;
        return this;
    }

    /**
     *  設置右菜單的布局
     * @param view 右菜單布局
     * @return 當類
     */
    public SideLayout setRightMenuView(View view){
        return setRightMenuView(view, DEFAULT_SCALE);
    }

    public SideLayout setRightMenuView(View view, float rightScale){
        rightMenuUI = new RightMenuUI(getContext(), view);
        this.rightScale = rightScale;
        return this;
    }

    /**
     *  設置正文布局
     * @param view 正文布局
     * @return 返回當類
     */
    public SideLayout setContentView(View view){
        contentUI = new ContentUI(getContext(), view);
        return this;
    }

    /**
     * 提交配置,必須調用
     */
    public void commit() {
        removeAllViews();
        if(leftMenuUI != null){
            leftMenuUI.calculate(leftScale, rightScale);
            measureWidth += leftMenuUI.width;
            addView(leftMenuUI.contentView, new LayoutParams(leftMenuUI.width, LayoutParams.MATCH_PARENT));
        }
        if(contentUI != null){
            contentUI.calculate(leftScale, rightScale);
            measureWidth += contentUI.width;
            addView(contentUI.contentView, new LayoutParams(contentUI.width, LayoutParams.MATCH_PARENT));
        }
        if(rightMenuUI != null){
            rightMenuUI.calculate(leftScale, rightScale);
            measureWidth += rightMenuUI.width;
            addView(rightMenuUI.contentView, new LayoutParams(rightMenuUI.width, LayoutParams.MATCH_PARENT));
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                mTouchX = event.getX();
                mMoveX = event.getX();
                return true;
            case MotionEvent.ACTION_MOVE:
                int dx = (int) (event.getX() - mMoveX);
                if(dx > 0){
                    //右滑
                    if(mScroller.getFinalX() > 0){
                        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -dx, 0);
                    }else{
                        mScroller.setFinalX(0);
                    }
                }else{
                    //左滑
                    if(mScroller.getFinalX() + Util.getScreenWidth(getContext()) - dx < measureWidth){
                        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -dx, 0);
                    }else{
                        mScroller.setFinalX((int) (measureWidth - Util.getScreenWidth(getContext())));
                    }
                }
                mMoveX = event.getX();
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                toTargetUI((int) (event.getX() - mTouchX));
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     *  滑動切換到目標的UI界面
     * @param dx 手指抬起時相比手指落下,滑動的距離
     */
    private void toTargetUI(int dx){
        int scrollX = mScroller.getFinalX();
        if(dx > 0){
            //右滑
            if(leftMenuUI != null){
                if(scrollX >= leftMenuUI.openX && scrollX < leftMenuUI.stopX){
                    contentUI.show(mScroller);
                }else if(scrollX >= leftMenuUI.startX && scrollX < leftMenuUI.openX){
                    leftMenuUI.show(mScroller);
                }
            }
            if(rightMenuUI != null){
                if(scrollX >= rightMenuUI.closeX){
                    rightMenuUI.show(mScroller, (int) measureWidth);
                }else if(scrollX >= contentUI.startX && scrollX < rightMenuUI.closeX){
                    contentUI.show(mScroller);
                }
            }
        }else{
            //左滑
            if(leftMenuUI != null){
                if(scrollX > leftMenuUI.startX && scrollX <= leftMenuUI.closeX){
                    leftMenuUI.show(mScroller);
                }else if(scrollX > leftMenuUI.closeX && scrollX < leftMenuUI.stopX){
                    contentUI.show(mScroller);
                }
            }
            if(rightMenuUI != null){
                if(scrollX > contentUI.startX && scrollX <= rightMenuUI.openX){
                    contentUI.show(mScroller);
                }else if(scrollX > rightMenuUI.openX){
                    rightMenuUI.show(mScroller, (int) measureWidth);
                }
            }
        }
    }

    @Override
    public void computeScroll(){
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
        super.computeScroll();
    }
}

在ACTION_MOVE操作中,根據移動的偏移量來滑動控件,這裡需要特別注意左邊的0臨界值和右邊的measureWidth測量寬度的臨界值,不然會滑出屏幕之外喲。最重要的方法還是toTargetUI這個方法,我單獨把這個方法剔出來講解。
/**
     *  滑動切換到目標的UI界面
     * @param dx 手指抬起時相比手指落下,滑動的距離
     */
    private void toTargetUI(int dx){
        int scrollX = mScroller.getFinalX();
        if(dx > 0){
            //右滑
            if(leftMenuUI != null){
                if(scrollX >= leftMenuUI.openX && scrollX < leftMenuUI.stopX){
                    contentUI.show(mScroller);
                }else if(scrollX >= leftMenuUI.startX && scrollX < leftMenuUI.openX){
                    leftMenuUI.show(mScroller);
                }
            }
            if(rightMenuUI != null){
                if(scrollX >= rightMenuUI.closeX){
                    rightMenuUI.show(mScroller, (int) measureWidth);
                }else if(scrollX >= contentUI.startX && scrollX < rightMenuUI.closeX){
                    contentUI.show(mScroller);
                }
            }
        }else{
            //左滑
            if(leftMenuUI != null){
                if(scrollX > leftMenuUI.startX && scrollX <= leftMenuUI.closeX){
                    leftMenuUI.show(mScroller);
                }else if(scrollX > leftMenuUI.closeX && scrollX < leftMenuUI.stopX){
                    contentUI.show(mScroller);
                }
            }
            if(rightMenuUI != null){
                if(scrollX > contentUI.startX && scrollX <= rightMenuUI.openX){
                    contentUI.show(mScroller);
                }else if(scrollX > rightMenuUI.openX){
                    rightMenuUI.show(mScroller, (int) measureWidth);
                }
            }
        }
    }

首先,根據ACTION_UP傳遞進來dx參數進入滑動方向的判斷,這個非常重要,不同的滑動方向對於處在不同scrollX的控件來說,操作目的是不一樣。我們以右滑為例,如果leftMenuUI為空,就代表用戶只想要左滑功能,rightMenuUI為空,就代表用戶只想要右滑功能。leftMenuUI和rightMenuUI都不為空,表示用戶同時想要左滑和右滑的功能。 然後對於不同的坐標區別進行判斷,並顯示出對應的UI界面。這裡文字一時半會兒說不明白。大家把我上面的圖拿來對比代碼進行分析,一會兒就看明白啦。   還有一個工具方法,是為了得到屏幕寬度的:
/**
 * Created by ccwxf on 2016/6/14.
 */
public class Util {

    public static int getScreenWidth(Context context){
        return context.getResources().getDisplayMetrics().widthPixels;
    }
}

  OK,自定義控件的源代碼就上面這6個文件,接下來講講怎麼使用。 首先,我們准備三個布局文件,分別代表左菜單布局、正文內容布局和右菜單布局。   left.xml:  



    

content.xml  



    


right.xml  



    

        
            
            
            
        

    

然後我們需要一個activity_main布局用戶呈現Activity(這不廢話麼。。。)



    

最後一步了,在代碼中調用:如果你只想要左菜單,那麼就只調用setLeftMenuView,如果只想要右菜單,那麼就只調用setRightMenuView,如果左菜單和右菜單都想要,那麼就一起調用。(setContentView必須調用哈,別問我為什麼難過
public class MainActivity extends Activity {

    private View leftView;
    private View contentView;
    private View rightView;

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

        initUI();

        SideLayout sideLayout = (SideLayout) findViewById(R.id.sideLayout);
        sideLayout.setLeftMenuView(leftView).setContentView(contentView).setRightMenuView(rightView).commit();
    }

    private void initUI(){
        leftView = View.inflate(this, R.layout.left, null);
        contentView = View.inflate(this, R.layout.content, null);
        rightView = View.inflate(this, R.layout.right, null);
        //初始化左邊菜單
        ListView listView = (ListView) leftView.findViewById(R.id.listView);
        listView.setAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, new String[]{
                "123","456","789","101","112","123","456","789","101","112","123","456","789","101","112","123","456","789","101","112"
        }));
        //初始化正文內容
        ViewPager viewPager = (ViewPager) contentView.findViewById(R.id.viewPager);
        viewPager.setAdapter(new TestDemoAdapter());
    }

    public class TestDemoAdapter extends PagerAdapter{

        private ImageView[] imageViews = new ImageView[5];

        public TestDemoAdapter() {
            for(int i = 0; i < imageViews.length; i++){
                imageViews[i] = new ImageView(MainActivity.this);
                imageViews[i].setImageResource(R.mipmap.ic_launcher);
            }
        }

        @Override
        public int getCount() {
            return 5;
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            container.addView(imageViews[position]);
            return imageViews[position];
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView(imageViews[position]);
        }
    }

}

好了,講解完了。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved