Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android仿小米商城商品詳情界面UI,ScrollView嵌套ScrollView/WebView/ListView

Android仿小米商城商品詳情界面UI,ScrollView嵌套ScrollView/WebView/ListView

編輯:關於Android編程

最近公司沒事,研究了下多嵌套滾動組件的事件分發,雖然以前也接觸過,但都是拿網上的用,也是特別簡單的,正好朋友也需要,就研究了下。這個Demo也不是很完善,放上來也是讓各位大牛給指點一下,優化優化

使用情景:

小米商城商品詳情界面,界面看似ScrollView,但當正常滾動到底部時,提示繼續上拉顯示更多詳情,上拉後直接滾動到第二屏,第二屏是個ViewPager,ViewPager裡面的各個pager有的是WebView有的是ListView,有的是ScrollView,一開始想想就特別頭暈,後來理清思路後,實現起來卻處處碰壁,不是ViewPager不能左右滑動就是ListView不能上拉,網上也搜索了很多相關Demo,但都沒有完善一點的,也許根本沒幾個人使用這樣的無腦嵌套吧,好吧,既然這樣,就只有自己動手了。

花了1周時間,總算出來點效果了,重寫了幾個組件:InnerScrollView、InnerWebView、InnerListView

 

一、InnerScrollView.java

思路:

如果內部ScrollView是固定高度,那麼需要滾動,外部的當然也需要滾動,所以要判斷當內部滾動到頂部並且手指繼續下滑時,把事件交父類處理,同樣當滾動到底部並繼續上滑時也要交出去,如果InnerScrollView的ChildView高度小於等於InnerScrollView高度(就是不出現滾動條)時,把事件交給父類處理。

實現:

只需要在onTouchEvent()裡做判斷即可,其他不重寫
package com.wuguangxin.morescrolldemo.view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ScrollView;

/**
 * 內部ScrollView,解決滑動內部ScrollView時,觸發外部滾動問題
 *
 * @author wuguangxin
 * @date 16/7/1 上午10:34
 */
public class XinInnerScrollView extends ScrollView {
    private final String TAG = "XinInnerScrollView";
    private float childHeight = 0;
    private float downX, downY; // 按下時
    private float currX, currY; // 移動時
    private float moveY; // 從按下到移動的Y距離
    private float scrollViewHeight;
    private boolean isOnTop; // ScrollView是否處於屏幕頂端
    private boolean isOnBottom; // ScrollView是否處於屏幕底端
    private boolean debug = true;
    private Position position = Position.NONE;

    public XinInnerScrollView(Context context) {
        this(context, null);
    }

    public XinInnerScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public XinInnerScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
            downX = ev.getX();
            downY = ev.getY();
            childHeight = getChildAt(0).getMeasuredHeight();
            scrollViewHeight = getHeight();
            break;
        case MotionEvent.ACTION_MOVE:
            currX = ev.getX();
            currY = ev.getY();
            moveY = Math.abs(currY - downY);
            isOnTop = getScrollY() == 0;
            isOnBottom = (getScrollY() + scrollViewHeight) == childHeight;
            // 垂直滑動
            if (moveY > Math.abs(currX - downX)) {
                if (childHeight <= scrollViewHeight) {
                    printLog("onTouchEvent ACTION_MOVE 不能滾動 父處理");
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                } else if (isOnTop) { // 當前處於ScrollView頂部
                    if (currY - downY > 0) {
                        printLog("onTouchEvent ACTION_MOVE 已到頂部 下滑 父處理");
                        getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    } else {
                        printLog("onTouchEvent ACTION_MOVE 已到頂部 上滑 子處理");
                    }
                } else if (isOnBottom) {
                    // 當前處於ScrollView底部
                    if (currY - downY < 0) {
                        printLog("onTouchEvent ACTION_MOVE 已到底部 上滑 父處理");
                        getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    } else {
                        printLog("onTouchEvent ACTION_MOVE 已到底部 下滑 子處理");
                    }
                } else {
                    // 當前處於ScrollView中間
                    printLog("onTouchEvent ACTION_MOVE 在中間 子處理");
                }
            }
            // 水平滾動
            else {
                if(position.equals(Position.TOP)){
                    printLog("onTouchEvent ACTION_MOVE 水平滾動 position=TOP 子處理");
                } else {
                    if(Math.abs(currX - downX) > 30){
                        printLog("onTouchEvent ACTION_MOVE 水平滾動 position!=TOP 橫向滑動距離>30 父處理");
                        getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    } else {
                        printLog("onTouchEvent ACTION_MOVE 水平滾動 position!=TOP 橫向滑動距離<=30 子處理");
                    }
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            printLog("onTouchEvent ACTION_UP ========================");
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
            break;
        }
        return super.onTouchEvent(ev);
    }

    /**
     * 為了更好的處理手勢滑動事件,設置該組件所處的位置;
     * 比如只有上下兩屏時,如果該View是在第一屏,那麼設置為Position.TOP,如果在第二屏,則設置為Position.BOTTOM
     *
     * @param position
     */
    public void setPosition(Position position) {
        this.position = position;
    }

    public static enum Position {
        /**
         * 頂部View,橫向滑動時將不考慮將事件交給父View。(該設計只為第一屏為純ScrollView考慮)
         */
        TOP,
        /**
         * 底部View, 橫向滑動時,將把事件交給父View處理
         */
        BOTTOM,
        /**
         * 不設置,將自動判斷(自動判斷並不是很精准)
         */
        NONE
    }

    public void printLog(String msg) {
        if (debug) {
            Log.d(TAG, msg);
        }
    }
}


說一下Position,因為第一屏或者第二屏中的ViewPager裡面也可能用到InnerScrollView,ViewPager裡面的需要考慮左右滑動的事件,但第一屏是不需要的,為了在第一屏做橫向滑動時(一般第一屏應該只有一個ScrollView),不把事件交給父類,所以需要知道該InnerScrollView是在哪裡使用的,設置該標記,做更好的判斷。日志中“子處理”處只打日志,不設置getParent().getParent().requestDisallowInterceptTouchEvent(true);是因為在ACTION_DOWN時已經告訴父類不要攔截,只需要在移動時在適合的條件下通知父類自己不再處理。這就是重寫的內部ScrollView。

還需要解決的問題

如果准備滾動到底部時,這時不抬起手指繼續往回滑,這時事件已經交出去了,往回滑動時,內部ScrollView已經無法滾動了,手勢如圖:\

二、InnerWebView.java

思路:

垂直滑動: 當處於頂部,繼續下滑時,交出事件; 當處於底部,繼續上滑時,交出事件; 水平滑動: 當處於左側,繼續右滑時,交出事件; 當處於右側,繼續左滑時,交出事件;

實現

 

package com.wuguangxin.morescrolldemo.view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.webkit.WebView;

/**
 * 內部WebView, 該View只適合放在最後一屏
 * 
 * @author wuguangxin
 * @date 16/7/1 上午10:34
 */
public class XinInnerWebView extends WebView {
    private final String TAG = "XinInnerScrollView";
    private boolean debug = true;
    private float downX, downY; // 按下時
    private float currX, currY; // 移動時
    private float moveX; // 移動長度-橫向

    public XinInnerWebView(Context context) {
        super(context);
    }

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

    public XinInnerWebView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
            printLog("onTouchEvent ACTION_DOWN");
            downX = ev.getX();
            downY = ev.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            currX = ev.getX();
            currY = ev.getY();
            moveX = Math.abs(currX - downX);
            printLog("onTouchEvent ACTION_MOVE getScrollX()="+getScrollX() + "  getScrollY()="+getScrollY());
            // 垂直滑動
            if (Math.abs(currY - downY) > moveX) {
                // 處於頂部或者無法滾動,並且繼續下滑,交出事件(currY-downY  >0是下滑, <0則是上滑)
                if (getScrollY() == 0 && currY - downY > 0) {
                    printLog("onTouchEvent ACTION_MOVE 在頂部 下滑 父處理");
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                }
                // 已到底部且繼續上滑時,把事件交出去
                else if(getContentHeight()*getScale() - (getHeight() + getScrollY()) <= 1 && currY - downY < 0){
                    printLog("onTouchEvent ACTION_MOVE 在底部 上滑 父處理");
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                }
            }
            // 水平滾動,橫向滑動長度大於20像素時再交出去,不然都當做是垂直滑動。
            else if(moveX > 20){
                // 橫向滑動事不直接交出去,是因為可能頁面出現水平滾動條,就是網頁寬度比屏幕還寬的情況下就需要判斷滑到左邊和滑到右邊的情況。
                // printLog("onTouchEvent ACTION_MOVE 橫向滑動 父處理");
                // getParent().getParent().requestDisallowInterceptTouchEvent(false);

                // 已在左邊且繼續右滑時,把事件交出去(currX - downX  >0是右滑, <0則是左滑)
                if (getScrollX() == 0 && currX - downX > 0) {
                    printLog("onTouchEvent ACTION_MOVE 在左邊 右滑 父處理");
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                }
                // 已在右邊且繼續左滑時,把事件交出去
                else if(getRight()*getScale() - (getWidth() + getScrollX()) <= 1 && currX - downX < 0){
                    printLog("onTouchEvent ACTION_MOVE 在右邊 左滑 父處理");
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                }
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            printLog("onTouchEvent ACTION_UP");
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
            break;
        }
        return super.onTouchEvent(ev);
    }

    public void printLog(String msg) {
        if (debug) {
            Log.d(TAG, msg);
        }
    }
}

 

條件判斷說明

怎麼判斷已經滑到左側或頂部呢,通過文檔和打印的日志發現,getScrollX()或者getScrollY()是當前可視界面滾動的距離X或者Y軸的距離,也就是說如果getScrollX()=0就是到左側,getScrollY()=0就是頂部,這個判斷方法在ScrollView中有效。

滑到底部的判斷,我也是參考了很多博客,可以查考這裡的介紹
滑到右側的判斷,根據滑到底部的邏輯,就可以很明白,把Y都改為X不就行了,但發現getContentHeight()是整個html的高度,而沒有getContentWidth()方法,是不是很郁悶?我們可以使用getRight()來代替,就是整個webView相對於父類的位置。可能這種方法不是很准確,但實際還是很適合的。
有人可能有疑問,干嘛*getScale()呢?因為webView是可以縮放界面的,縮放後整個html的大小都發生了變化。

三、InnerListView.

說明

不支持那種水平滑動Item出現隱藏布局這種特殊情況;不支持下拉刷新,因為下拉刷新和外部滾動視圖的滑動沖突。

思路

當水平滑動時:
把事件交給了父類處理;
當垂直滑動時:
已到第一條並完全顯示,且繼續下滑,則事件交給父類;已到最後一條並完全顯示,不會把事件交給父類,而是回調指定的接口,就是一般下拉組件當最後一刻item可見時觸發的事件一樣,在這裡可以去加載更多數據等;

實現

 

 

package com.wuguangxin.morescrolldemo.view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.ListView;

/**
 * 內部ListView, 該組件不支持下拉刷新,上拉加載更多,是為了嵌套在ScrollView或者ViewPager中的,適合數據少的環境使用,最好是一次性顯示所有數據
 * 提供一個接口OnLastItemVisibleListener,當最後一個item完全顯示時,回調onLastItemVisible(),可以去加載更多數據。
 *
 * @author wuguangxin
 * @date 16/7/1 上午10:34
 */
public class XinInnerListView extends ListView implements AbsListView.OnScrollListener {
    private final String TAG = "XinInnerScrollView";
    private boolean debug = true;
    private boolean isFirstItemVisible; // 第一個item是否可見
    private boolean isLastItemVisible; // 最後一個item是否可見
    private int downX, downY; // 按下時
    private int currX, currY; // 移動時
    private int moveY; // 從按下到移動的Y距離

    public XinInnerListView(Context context) {
        super(context);
        setOnScrollListener(this);
    }

    public XinInnerListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOnScrollListener(this);
    }

    public XinInnerListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOnScrollListener(this);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
            downX = (int)ev.getX();
            downY = (int)ev.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            currX = (int)ev.getX();
            currY = (int)ev.getY();
            moveY = Math.abs(currY - downY);
            if(currY == downY){
                break;
            }
            // 垂直滑動
            if (moveY > Math.abs(currX - downX)) {
                if (isFirstItemVisible) { // 當前處於頂部
                    if (currY - downY > 0) {
                        printLog("onTouchEvent ACTION_MOVE 已到頂部 下滑 父處理");
                        getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    } else {
                        printLog("onTouchEvent ACTION_MOVE 已到頂部 上滑 子處理");
                    }
                } else if (isLastItemVisible) {
                    // 當前處於底部
                    if (currY - downY < 0) {
                        printLog("onTouchEvent ACTION_MOVE 已到底部 上滑 父處理");
                        getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    } else {
                        printLog("onTouchEvent ACTION_MOVE 已到底部 下滑 子處理");
                    }
                } else {
                    // 當前處於中間
                    printLog("onTouchEvent ACTION_MOVE 在中間 子處理");
                }
            } else {
                // 水平滾動
                printLog("onTouchEvent ACTION_MOVE 水平滾動 父處理");
                getParent().getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            printLog("onTouchEvent ACTION_UP ========================");
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
            break;
        }
        return super.onTouchEvent(ev);
    }

    /**
     * 判斷最後listView中最後一個item是否完全顯示出來
     * @return
     */
    protected boolean isLastItemVisible() {
        Adapter adapter = getAdapter();
        if (null == adapter || adapter.isEmpty()) {
            return true;
        }
        int lastVisiblePosition = getLastVisiblePosition();
        if (lastVisiblePosition >= (adapter.getCount() - 1) - 1) {
            View lastVisibleChild = getChildAt(Math.min(lastVisiblePosition - getFirstVisiblePosition(), getChildCount() - 1));
            if (lastVisibleChild != null) {
                return lastVisibleChild.getBottom() <= getBottom();
            }
        }
        return false;
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        isFirstItemVisible = firstVisibleItem == 0 && getScrollY() == 0;
        isLastItemVisible = isLastItemVisible();
        if (isLastItemVisible) {
            if (onLastItemVisibleListener != null) {
                onLastItemVisibleListener.onLastItemVisible(view, firstVisibleItem + visibleItemCount - 1, getAdapter());
            }
        }
    }

    public void setOnLastItemVisibleListener(OnLastItemVisibleListener onLastItemVisibleListener) {
        this.onLastItemVisibleListener = onLastItemVisibleListener;
    }

    private OnLastItemVisibleListener onLastItemVisibleListener;

    /**
     * 最後一個Item顯示的監聽器
     */
    public interface OnLastItemVisibleListener {
        void onLastItemVisible(AbsListView view, int position, Adapter adapter);
    }

    public void printLog(String msg) {
        if (debug) {
            Log.d(TAG, msg);
        }
    }
}

 

在代碼中出現很多沒有代碼的else,只是為了看日志方便而已。

 

四、ScrollView 嵌套 ScrollView

概述

白色區域是可以單獨滾動的

\

Activity

ScrollViewInScrollViewActivity.java

public class ScrollViewInScrollViewActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scrollview);
        setTitle("ScrollView In ScrollView");
    }
}

 

xml

activity_scrollview.xml




    

        

        

            

        

        

    

 

五、ScrollView嵌套WebView

 

概述

黑色背景是原生ScrollView,中間是InnerWebView,該WebView設置了固定高度,所以可以上下滑動,如果高度設置為wrap_content,則會把整個網頁撐開,與外部ScrollView融合在一起。

\

Activity

WebViewInScrollViewActivity.java
 

package com.wuguangxin.morescrolldemo.ui;

import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.wuguangxin.morescrolldemo.Configs;
import com.wuguangxin.morescrolldemo.R;
import com.wuguangxin.morescrolldemo.view.XinInnerWebView;

/**
 * WebView In ScrollView
 * 
 * @author wuguangxin
 * @date 16/7/5 上午11:35
 */
public class WebViewInScrollViewActivity extends FragmentActivity {
    private XinInnerWebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_webview);
        setTitle("WebView In ScrollView");

        mWebView = (XinInnerWebView) findViewById(R.id.webview);
        mWebView.loadUrl(Configs.URL_BAIDU);
        initWebView();
    }

    private void initWebView(){
        WebSettings webSet = mWebView.getSettings();
        webSet.setSupportZoom(true); // 支持縮放
        webSet.setAllowFileAccess(true); // 設置可以訪問文件
        webSet.setJavaScriptEnabled(true); // 啟用JavaScript
        webSet.setBlockNetworkImage(false); // 限制網絡圖片
        webSet.setBuiltInZoomControls(true); // 控制頁面縮放
        webSet.setLoadWithOverviewMode(true); // 設置webview加載的頁面的模式,
        webSet.setDefaultZoom(WebSettings.ZoomDensity.MEDIUM); // 設置默認的縮放級別
        webSet.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
        mWebView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url){
                view.loadUrl(url);
                return true;
            }

            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon){
            }

            @Override
            public void onPageFinished(WebView view, String url){
            }
        });
    }
}

 

xml

activity_webview.xml




    

        

        

        

    

 

六、ScrollView嵌套ListView

概述

\

Activity

ListViewInScrollViewActivity.java

package com.wuguangxin.morescrolldemo.ui;

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.BaseAdapter;
import android.widget.TextView;

import com.wuguangxin.morescrolldemo.R;
import com.wuguangxin.morescrolldemo.view.XinInnerListView;

import java.util.ArrayList;
import java.util.List;

/**
 * ListView In ScrollView
 * 
 * @author wuguangxin
 * @date 16/7/5 上午10:59
 */
public class ListViewInScrollViewActivity extends FragmentActivity {
    private int maxListSize = 300;
    private List list;
    private MyAdapter mAdapter;
    private int i;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listview);
        setTitle("ListView In ScrollView");

        XinInnerListView mListView = (XinInnerListView) findViewById(R.id.listview);
        list = getList();
        mAdapter = new MyAdapter(this, list);
        mListView.setAdapter(mAdapter);
        mListView.setOnLastItemVisibleListener(new XinInnerListView.OnLastItemVisibleListener() {
            @Override
            public void onLastItemVisible(AbsListView view, int position, Adapter adapter) {
                if(list.size() < maxListSize){
                    list.addAll(getList());
                    mAdapter.setData(list);
                    mAdapter.notifyDataSetChanged();
                }
            }
        });
    }

    private List getList(){
        if(list == null){
            list = new ArrayList<>();
        }
        List tempList = new ArrayList<>();
        for (i = 0; i < 50; i++) {
            tempList.add("item " + (this.list.size() + i+1) + " / "+maxListSize);
        }
        return tempList;
    }

    public class MyAdapter extends BaseAdapter {
        private List list;
        private Context context;

        public MyAdapter(Context context, List list) {
            this.context = context;
            this.list = list;
        }

        @Override
        public int getCount() {
            return list == null ? 0 : list.size();
        }

        @Override
        public String getItem(int position) {
            return list == null ? null : list.get(position);
        }

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

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            Holder holder = null;
            if(convertView == null){
                holder = new Holder();
                convertView = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, null);
                holder.mTextView = (TextView)convertView;
                convertView.setTag(holder);
            } else {
                holder = (Holder) convertView.getTag();
            }
            holder.mTextView.setText(getItem(position));
            return convertView;
        }

        public void setData(List list) {
            this.list = list;
        }

        class Holder {
            private TextView mTextView;
        }
    }
}

 

xml

activity_listview.java




    

        

        

        

    

 


 

七、仿小米商城App商鋪詳情界面

概述

該界面仿小米商城App商品詳情界面,也類似蘑菇街、京東、淘寶等界面。

如下面原型圖




本Demo效果圖:


思路
 

實現

~~~~~還沒完 待續 ~~~~~

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