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

最近公司没事,研究了下多嵌套滚动组件的事件分发,虽然以前也接触过,但都是拿网上的用,也是特别简单的,正好朋友也需要,就研究了下

这个Demo也不是很完善,放上来也是让各位大牛给指点一下,优化优化

使用情景:

小米商城商品详情界面,界面看似ScrollView,但当正常滚动到底部时,提示继续上拉显示更多详情,上拉后直接滚动到第二屏,第二屏是个ViewPager,ViewPager里面的各个pager有的是WebView有的是ListView,有的是ScrollView,一开始想想就特别头晕,后来理清思路后,实现起来却处处碰壁,不是ViewPager不能左右滑动就是ListView不能上拉,网上也搜索了很多相关Demo,但都没有完善一点的,也许根本没几个人使用这样的无脑嵌套吧,好吧,既然这样,就只有自己动手了。

花了1周时间,总算出来点效果了,重写了几个组件:InnerScrollView、InnerWebView、InnerListView


一、InnerScrollView.java

  1. 思路:

    如果内部ScrollView是固定高度,那么需要滚动,外部的当然也需要滚动,所以要判断当内部滚动到顶部并且手指继续下滑时,把事件交父类处理,同样当滚动到底部并继续上滑时也要交出去,如果InnerScrollView的ChildView高度小于等于InnerScrollView高度(就是不出现滚动条)时,把事件交给父类处理。

  2. 实现:

    只需要在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。
  3. 还需要解决的问题

    如果准备滚动到底部时,这时不抬起手指继续往回滑,这时事件已经交出去了,往回滑动时,内部ScrollView已经无法滚动了,手势如图:

二、InnerWebView.java

  1. 思路:

    垂直滑动:      
    • 当处于顶部,继续下滑时,交出事件;
    • 当处于底部,继续上滑时,交出事件;
    水平滑动:      
    • 当处于左侧,继续右滑时,交出事件;
    • 当处于右侧,继续左滑时,交出事件;
  2. 实现

    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);
            }
        }
    }

  3. 条件判断说明

    • 怎么判断已经滑到左侧或顶部呢,通过文档和打印的日志发现,getScrollX()或者getScrollY()是当前可视界面滚动的距离X或者Y轴的距离,也就是说如果getScrollX()=0就是到左侧,getScrollY()=0就是顶部,这个判断方法在ScrollView中有效。

    • 滑到底部的判断,我也是参考了很多博客,可以查考这里的介绍

    • 滑到右侧的判断,根据滑到底部的逻辑,就可以很明白,把Y都改为X不就行了,但发现getContentHeight()是整个html的高度,而没有getContentWidth()方法,是不是很郁闷?我们可以使用getRight()来代替,就是整个webView相对于父类的位置。可能这种方法不是很准确,但实际还是很适合的。

    有人可能有疑问,干嘛*getScale()呢?因为webView是可以缩放界面的,缩放后整个html的大小都发生了变化。

三、InnerListView.

  1. 说明

    • 不支持那种水平滑动Item出现隐藏布局这种特殊情况;
    • 不支持下拉刷新,因为下拉刷新和外部滚动视图的滑动冲突。
  2. 思路

    当水平滑动时:
    • 把事件交给了父类处理;

    当垂直滑动时:
    • 已到第一条并完全显示,且继续下滑,则事件交给父类;
    • 已到最后一条并完全显示,不会把事件交给父类,而是回调指定的接口,就是一般下拉组件当最后一刻item可见时触发的事件一样,在这里可以去加载更多数据等;
  3. 实现

    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

  1. 概述

    白色区域是可以单独滚动的


  2. 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");
        }
    }
    

  3. xml

    activity_scrollview.xml

    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true" >
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dip"
                android:background="#333"
                android:padding="15dip"
                android:text="head\nScrollView嵌套ScrollView"
                android:textColor="#fff"/>
    
            <com.wuguangxin.morescrolldemo.view.XinInnerScrollView
                android:layout_width="match_parent"
                android:layout_height="400dip"
                android:background="#fff">
    
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:padding="15dip"
                    android:lineSpacingExtra="3dp"
                    android:lineSpacingMultiplier="1"
                    android:text="Inner ScrollView \n\n可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。\n\n\n\n\n\n"
                    android:textColor="#333"/>
    
            </com.wuguangxin.morescrolldemo.view.XinInnerScrollView>
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="1000dip"
                android:background="#333"
                android:padding="30dip"
                android:lineSpacingExtra="3dp"
                android:lineSpacingMultiplier="1.5"
                android:text="foot \n\n可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。\n\n\n\n\n\n"
                android:textColor="#fff"/>
    
        </LinearLayout>
    </ScrollView>
    

  五、ScrollView嵌套WebView

  1. 概述

    黑色背景是原生ScrollView,中间是InnerWebView,该WebView设置了固定高度,所以可以上下滑动,如果高度设置为wrap_content,则会把整个网页撑开,与外部ScrollView融合在一起。


  2. 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){
                }
            });
        }
    }
    

  3. xml

    activity_webview.xml

    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true" >
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="200dip"
                android:background="#333"
                android:padding="15dip"
                android:text="head\nScrollView嵌套WebView"
                android:textColor="#fff"/>
    
            <com.wuguangxin.morescrolldemo.view.XinInnerWebView
                android:id="@+id/webview"
                android:layout_width="match_parent"
                android:layout_height="400dip"/>
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="1000dip"
                android:background="#333"
                android:padding="30dip"
                android:lineSpacingExtra="3dp"
                android:lineSpacingMultiplier="1.5"
                android:text="foot \n\n可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。\n\n\n\n\n\n"
                android:textColor="#fff"/>
    
        </LinearLayout>
    </ScrollView>

六、ScrollView嵌套ListView

  1. 概述


  2. 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<String> 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<String> getList(){
            if(list == null){
                list = new ArrayList<>();
            }
            List<String> 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<String> list;
            private Context context;
    
            public MyAdapter(Context context, List<String> 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<String> list) {
                this.list = list;
            }
    
            class Holder {
                private TextView mTextView;
            }
        }
    }
    

  3. xml

    activity_listview.java

    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#333"
        android:fillViewport="true" >
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="15dip"
                android:text="==head==\nScrollView嵌套ListView"
                android:textColor="#ccc"/>
    
            <com.wuguangxin.morescrolldemo.view.XinInnerListView
                android:id="@+id/listview"
                android:layout_width="match_parent"
                android:layout_height="400dip"
                android:layout_marginLeft="15dip"
                android:layout_marginRight="15dip"
                android:background="#55ffffff"/>
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="1000dip"
                android:padding="15dip"
                android:lineSpacingExtra="3dp"
                android:lineSpacingMultiplier="1.5"
                android:text="==foot== \n可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。可以看到其内部包含很多变量,这些变量的值一般来源于用户输入和global.xml.ftl中预定义的值,经过recipe.xml.ftl中instantiate标签的处理,将变量换成实际的值,即可在我们的项目的指定位置,得到我们期望的Activity。\n\n\n\n\n\n"
                android:textColor="#ccc"/>
    
        </LinearLayout>
    </ScrollView>
    

      

七、仿小米商城App商铺详情界面

  1. 概述

    该界面仿小米商城App商品详情界面,也类似蘑菇街、京东、淘宝等界面。

    如下面原型图
        

    本Demo效果图:
        

  2. 思路

  3. 实现

    ~~~~~还没完 待续 ~~~~~

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值