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. 实现

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

时间: 2024-10-11 16:38:47

Android仿小米商城商品详情界面UI,ScrollView嵌套ScrollView/WebView/ListView的相关文章

Android仿小米商城底部导航栏之二(BottomNavigationBar、ViewPager和Fragment的联动使用)

简介 在前文<Android仿小米商城底部导航栏(基于BottomNavigationBar)>我们使用BottomNavigationBar控件模仿实现了小米商城底部导航栏效果.接下来更进一步的,我们将通过BottomNavigationBar控件和ViewPager空间的联动使用来实现主界面的滑动导航. 导航是移动应用最重要的方面之一,对用户体验是良好还是糟糕起着至关重要的作用.好的导航可以让一款应用更加易用并且让用户快速上手.相反,糟糕的应用导航很容易让人讨厌,并遭到用户的抛弃.为了打造

android内存优化5—对界面UI的优化(2)

在一个应用程序中,一般都会存在多个Activity,每个Activity对应着一个UI布局文件.一般来说,为了保持不同窗口之间的风格统一,在这些UI布局文件中,几乎肯定会用到很多相同的布局.如果我们在每个xml文件中都把相同的布局都重写一遍,一个是代码冗余,可读性很差:另一个是修改起来比较麻烦,对后期的修改和维护非常不利.所以,一般情况下,我们需要把相同布局的代码单独写成一个模块,然后在用到的时候,可以通过<include /> 标签来重用layout的代码. 常见的,有的应用在最上方会有一个

android内存优化4—对界面UI的优化(1)

在Android应用开发过程中,屏幕上控件的布局代码和程序的逻辑代码通常是分开的.界面的布局代码是放在一个独立的xml文件中的,这个文件里面是树型组织的,控制着页面的布局.通常,在这个页面中会用到很多控件,控件会用到很多的资源.Android系统本身有很多的资源,包括各种各样的字符串.图片.动画.样式和布局等等,这些都可以在应用程序中直接使用.这样做的好处很多,既可以减少内存的使用,又可以减少部分工作量,也可以缩减程序安装包的大小. 下面从几个方面来介绍如何利用系统资源. 1)利用系统定义的id

android内存优化6—对界面UI的优化(3)

本篇博文主要讨论一下复杂界面中常用的一种技术--界面延迟加载技术. 有时候,我们的页面中可能会包含一些布局,这些布局默认是隐藏的,当用户触发了一定的操作之后,隐藏的布局才会显示出来.比如,我们有一个Activity用来显示好友的列表,当用户点击Menu中的"导入"以后,在当前的Activity中才会显示出一个导入好友的布局界面.从需求的角度来说,这个导入功能,一般情况下用户是不使用的.即大部分时候,导入好友的布局都不会显示出来.这个时候,就可以使用延迟加载的功能. ViewStub是一

Android ScrollView嵌套ScrollView滚动的问题解决办法

引用:http://mengsina.iteye.com/blog/1707464 http://fenglog.com/article.asp?id=449 Android ScrollView嵌套ScrollView滚动的问题解决办法 原文地址:http://trivedihardik.wordpress.com/2011/09/19/scrollview-inside-scrollview-scrolling-problem/ 搞技术的多少看的懂E文,也不翻译了. While design

Android 仿Win8的metro的UI界面(上)

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/23441455 昨晚没事手机下载了一些APP,发现现在仿win8的主界面越来越多,在大家见惯了类GridView或者类Tab后,给人一种耳目一新的感觉.今天在eoe上偶然发现已经有人实现了这个功能的源码(地址:http://www.eoeandroid.com/forum.php?mod=viewthread&tid=327557),马上下载跑了一下,效果很炫,但是有些bug,比

ScrollView嵌套Viewpager和ListView的整合

1.ScrollView嵌套Viewpager解决滑动冲突: (1)重新ScrollView(法一) /**  * 能够兼容ViewPager的ScrollView  * @Description: 解决了ViewPager在ScrollView中的滑动反弹问题  */ public class ScrollViewExtend extends ScrollView {     // 滑动距离及坐标     private float xDistance, yDistance, xLast, y

Android仿微信语音聊天界面

有段时间没有看视频了,昨天晚上抽了点空时间,又看了下鸿洋大神的视频教程,又抽时间写了个学习记录.代码和老师讲的基本一样,网上也有很多相同的博客.我只是在AndroidStudio环境下写的. --主界面代码-- public class MainActivity extends Activity { private ListView mListView; private ArrayAdapter<Recorder> mAdapter; private List<Recorder>

转-Android仿微信气泡聊天界面设计

微信的气泡聊天是仿iPhone自带短信而设计出来的,不过感觉还不错可以尝试一下仿着微信的气泡聊天做一个Demo,给大家分享一下!效果图如下: 气泡聊天最终要的是素材,要用到9.png文件的素材,这样气泡会随着聊天内容的多少而改变气泡的大小且不失真.为了方便,我就直接在微信里面提取出来啦. 聊天的内容是用ListView来显示的,将聊天的内容封装成一个ChatMsgEntity类的对象里面,然后交给自定义的ListView适配器将聊天内容显示出来. ChatMsgEntity.java比较简单,只