Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義View之仿淘寶詳情頁

自定義View之仿淘寶詳情頁

編輯:關於Android編程

基本介紹

現在的一些購物類App例如淘寶,京東等,在物品詳情頁,都采用了類似分層的模式,即上拉加載詳情的方式,節省了空間,使用戶的體驗更加的舒適。只要對於某個東西的介紹很多時,都可以采取這樣的方式,第一個頁面顯示必要的,第二個頁面顯示詳細信息。

在之前寫項目的時候,曾經寫過一個類似淘寶詳情頁的UI效果,如下:

這裡寫圖片描述

仔細分析這種UI效果,其實很簡單,就是兩個頁面,垂直擺放,同時兩個頁面之間過渡時,加上一層特殊處理,及當第二個頁面顯示多少時,松開手指時復原或者直接顯示第二個頁面。

根據這種特殊的UI效果進行實現封裝,最終的效果如下:

這裡寫圖片描述

能夠實現頁面的切換,當滑動到第二個頁面不足顯示區域的三分之一時,則松開手指時會還原,如果超過三分之一,則會跳到第二個頁面。

同時實現了一些事件分發的處理,能夠嵌入按鈕,ViewPager等控件。

使用方式

編寫xml文件,添加控件




    

        

在實現中,一定要將控件的高度設置為match_parent,因為在代碼中需要獲取顯示區域的高度,用以判斷是否復原和跳轉頁面。

在該控件中添加兩個布局控件。

在Activity中查找控件並設置一些監聽。

public class DetailVerticalActivity extends AppCompatActivity {

    private DetailVerticalView v;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_taobao_detail);
        // 查找控件
        v = (DetailVerticalView) findViewById(R.id.detailVerticalView);
        // 設置滾動監聽的回調
        v.setScrollChangeListener(new DetailVerticalView.ScrollChangeListener() {

            @Override
            public void scrollY(int y) {
                // y軸滑動偏移量的回調
            }

            @Override
            public void onScollStateChange(int type) {
                // 滑動到那個頁面狀體的變化
                if(DetailVerticalView.TOP == type){
                    Toast.makeText(DetailVerticalActivity.this, "1", Toast.LENGTH_SHORT).show();
                }else if(DetailVerticalView.BOTTOM==type){
                    Toast.makeText(DetailVerticalActivity.this, "2", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }

    public void test(View view){
        Toast.makeText(getApplicationContext(),"點擊",Toast.LENGTH_SHORT).show();
    }
}

實現原理

在實現過程中,主要考慮以下幾點問題:

如何布置兩個頁面的控件位置。

解決方式是使自定義View實現ViewGroup,重寫onMeasure和onLayout方法,對子View進行布局。

如何判斷兩個頁面何時跳轉,

解決方式:獲取View顯示區域高度的三分之一,如果超過,則顯示第二個頁面,否則
則恢復原狀,不顯示第二個頁面。

如何實現滑動的過渡。

解決方式:使用Scroller進行滑動的控制。

判斷的一些邊界的問題。

滑動到底部和滑動到頂部時,等不可再滑,需要對邊界進行配置。

事件分發的處理。

對於點擊等事件,利用onInterceptTouchEvent() 對相關事件進行攔截。

代碼實現

創建對象,初始化控件

/**
 *
 * 仿淘寶詳情頁
 * Created by alex_mahao on 2016/8/29.
 */
public class DetailVerticalView extends ViewGroup {

    // 滑動到頂部的狀態
    public static final int TOP = 1;

    // 滑動到底部的狀態
    public static final int BOTTOM = 2;

    public static final String TAG = "DetailVerticalView";

    /**
     * 屏幕高度
     */
    private int mScreenHeight;

    /**
     * 手指上次觸摸事件的y軸位置
     */
    private int mLastY;

    /**
     * 點擊時y軸的偏移量
     */
    private int mScrollY;

    /**
     * 滾動控件
     */
    private Scroller mScroller;

    /**
     * 最小移動距離判定
     */
    private int mTouchSlop;

    /**
     * 滑動結束的偏移量
     */
    private int mEnd;

    /**
     * 初始按下y軸坐標
     */
    private int mDownStartY;

    /**
     * 記錄當前y軸坐標
     */
    private int y;

    /**
     * 控件的高度
     */
    private int mHeight;

    /**
     * 監聽的回調
     */
    private ScrollChangeListener scrollChangeListener;

    public DetailVerticalView(Context context) {
        super(context, null);
    }

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

    /**
     * 初始化方法
     */
    private void init() {

        // 創建滑對象,以便滑動時使用
        mScroller = new Scroller(getContext());

        // 獲取系統的最小距離
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(ViewConfiguration.get(getContext()));
    }

    //....
}

一些變量的定義後面會有,此處不提。

mTouchSlop,系統默認的最小距離。當手指滑動的大小大於該值時,則認為是滑動,不在是點擊,後面會通過與該值比對進行事件攔截。

測量自身的大小和兩個頁面控件的大小

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 獲取顯示區域的高度
        mScreenHeight = MeasureSpec.getSize(heightMeasureSpec);

        // 遍歷子View
        int count = getChildCount();
        // 控件的高度
        mHeight = 0;
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            // 測量子View 高度
            int childHeight = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
            measureChild(childView, widthMeasureSpec, childHeight);
            mHeight = getChildAt(i).getMeasuredHeight() + mHeight;
        }
        // 設置控件的高度
        setMeasuredDimension(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));

    }

設置自定義View的高度為match_parent,就是為了獲取顯示區域的高度,從此處可以看出。

測量子View的高度,便於後面的布局使用。在中間,會看到我對childView的高度設置了值childHeight,該值的目的是告訴子View,我給你一個很大的值,你看著自己需要多少自己設置就行,及AT_MOST模式

在最後的時候,設置當前控件的高度,為兩個頁面控件的高度之和。

對兩個頁面控件進行布置onLayout()方法


@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int childCount = getChildCount();
        int childHeight = 0;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                // 確定位置
                child.layout(l, childHeight, r, childHeight + child.getMeasuredHeight());
                childHeight = +child.getMeasuredHeight();
            }
        }

    }

這一段沒什麼難度,看著理解即可。

處理事件分發

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 獲取當前觸摸位置Y軸坐標
        y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 第一次按下時的Y軸坐標
                mDownStartY = (int) ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                // 如果大於mTouchSlop,認為是滑動,則不再讓子View處理事件
                if (Math.abs(y - mDownStartY) > mTouchSlop) {
                    // 因為是在onTouchEvent中處理,如果子View處理過事件,
                    // 則該控件的onTouchEvent()不再有DOWN事件,在這裡獲取一些值
                    mLastY = y;
                    mScrollY = getScrollY();
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return false;
    }

對於事件的傳遞,流程為 父:onInterceptTouchEventr - > 子:onTouchEvent() -> 父:onTouchEvent(),當然這裡只是寫了必要的流程。如果子:onTouchEvent()返回true,則當前控件就無法捕獲觸摸事件,那麼滑動就無從處理了。所以,在此處,我們判斷垂直滑動大於最小滑動距離時,就把事件給截斷,不在交給子控件處理。

同時,如果子控件處理了一些事件,那麼父控件的onTouchEvent()中,將不在有DOWN事件,那麼我們需要先獲取一次值,在打斷子View的時候。

滑動相關的處理

@Override
    public boolean onTouchEvent(MotionEvent event) {
        y = (int) event.getY();
        mScrollY = getScrollY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 如果通過事件攔截獲取到的觸摸,則直接就為Move方法
                mLastY = y;
                mScrollY = getScrollY();
                break;

            case MotionEvent.ACTION_MOVE:
                int dy = mLastY - y;

                Log.i(TAG,"dy:"+dy+"-"+"mLastY:"+mLastY+"-"+"mScrolly:"+mScrollY);

                if(mScrollY+dy<0){
                    // 滑動到頂部,不可再滑動
                    scrollTo(0,0);
                }else if(mScrollY+dy>getMeasuredHeight()-mScreenHeight){
                    //底部時
                    scrollTo(0,getMeasuredHeight()-mScreenHeight);
                }else{
                    scrollBy(0, dy);
                    mLastY = y;
                }

                break;
            case MotionEvent.ACTION_UP:

                // 當前偏移量是
                int absScroll = mScrollY+mScreenHeight-getChildAt(0).getMeasuredHeight();

                if(absScroll<0||absScroll>mScreenHeight){
                    // 第一個頁面未滑到底部,不做操作,如果absScroll>屏幕的高度,則完全滾動
                    // 不做任何滾動操作
                }else if(absScroll>mScreenHeight/3){
                    // 監聽的回調
                    if(scrollChangeListener!=null){
                        scrollChangeListener.onScollStateChange(BOTTOM);
                    }
                    // 松開時顯示第二個頁面
                    mScroller.startScroll(0, mScrollY, 0, mScreenHeight-absScroll);
                }else if(absScroll<mscreenheight pre="" return="">

該段是整個自定義View中最重要的方法,總結來說干了兩件事情:

ACTION_MOVE中,處理觸摸滑動的事件,及手指在屏幕滑動時的相關處理。

邊界處理,判斷滑動時,如果到頂部和底部,則不再滑動,否則進行相關的滑動。

ACTION_UP:手指離開屏幕時,對是否需要跳轉進行判斷,如果需要跳轉,則跳轉。

在手指離開時,判斷當前頁面的顯示量,偏移量+顯示區域的高度-第一個控件的高度=第二個控件顯示的高度。

如果顯示的高度小於0,則表示還在第一個頁面,第二個頁面顯示都沒顯示,不做任何處理。

如果顯示的高度大於顯示區域的高度,則表示第二個頁面完全顯示了,不做任何處理。

否則,如果顯示的高度大於顯示區域的1/3,則跳轉到第二個頁面,小於,則恢復到第一個頁面。

可以看到通過mScroller.startScroll()實現頁面的滑動跳轉,使用該方式,需要重寫另一個方法

   @Override
    public void computeScroll() {
        super.computeScroll();
        //判斷scroller滾動是否完成
        if (mScroller.computeScrollOffset()) {
            // 這裡調用View的scrollTo()完成實際的滾動
            scrollTo(0, mScroller.getCurrY());
            //刷新試圖
            postInvalidate();
        }
    }

最後一步,設置一些必要的監聽回調,和輔助方法

  /**
     * 監聽的接口定義
     */
    public interface  ScrollChangeListener{
        void scrollY(int y);
        void onScollStateChange(int type);
    }

    /**
     * 設置監聽
     * @param scrollChangeListener
     */
    public void setScrollChangeListener(ScrollChangeListener scrollChangeListener) {
        this.scrollChangeListener = scrollChangeListener;
    }
    /**
     * 回到第一個頁面的頂部
     */
    public void scrollToTop(){
        mScroller.startScroll(0, getScrollY(), 0, -getScrollY());
        scrollChangeListener.onScollStateChange(TOP);
        postInvalidate();

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