自定义ViewGroup实现仿淘宝的商品详情页

最近公司在新版本上有一个需要, 要在首页添加一个滑动效果, 具体就是仿照X宝的商品详情页, 拉到页面底部时有一个粘滞效果,

如下图 X东的商品详情页,如果用户继续向上拉的话就进入商品图文描述界面:

刚开始是想拿来主义,直接从网上找个现成的demo来用, 但是网上无一例外的答案都特别统一: 几乎全部是ScrollView中再套两个ScrollView,或者是一个LinearLayout中套两个ScrollView。 通过指定父view和子view的focus来切换滑动的处理界面---即通过view的requestDisallowInterceptTouchEvent方法来决定是哪一个ScrollView来处理滑动事件。

使用以上方法虽然可以解一时之渴, 但是存在几点缺陷:

1  扩展性不强 : 如果后续产品要求不止是两页滑动呢,是三页滑动呢, 难道要嵌3个ScrollView并通过N个判断来实现吗

2  兼容性不强 : 如果需要在某一个子页中需要处理左右滑动事件或者双指操作事件呢, 此方法就无法实现了

3 个人原因 : 个人喜欢自己掌握主动性,事件的处理自己来控制更靠谱一些(PS:就如同一份感情一样,需要细心去经营^_^)

总和以上原因, 自己实现了一个ViewGroup,实现文章开头提到的效果, 废话不多说  直接上源码,以下只是部分主要源码,并对每一个方法都做了注释,可以参照注释理解。   文章最后对这个ViewGroup加了一点实现的细节以及如何使用此VIewGroup, 以及demo地址

package com.mcoy.snapscrollview;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * @author jiangxinxing---mcoy in English
 *
 * 了解此ViewGroup之前, 有两点一定要做到心中有数
 * 一个是对Scroller的使用, 另一个是对onInterceptTouchEvent和onTouchEvent要做到很熟悉
 * 以下几个网站可以做参考用
 * http://blog.csdn.net/bigconvience/article/details/26697645
 * http://blog.csdn.net/androiddevelop/article/details/8373782
 * http://blog.csdn.net/xujainxing/article/details/8985063
 */
public class McoySnapPageLayout extends ViewGroup {

        。。。。

	public interface McoySnapPage {
		/**
		 * 返回page根节点
		 *
		 * @return
		 */
		View getRootView();

		/**
		 * 是否滑动到最顶端
		 * 第二页必须自己实现此方法,来判断是否已经滑动到第二页的顶部
		 * 并决定是否要继续滑动到第一页
		 */
		boolean isAtTop();

		/**
		 * 是否滑动到最底部
		 * 第一页必须自己实现此方法,来判断是否已经滑动到第二页的底部
		 * 并决定是否要继续滑动到第二页
		 */
		boolean isAtBottom();
	}

	public interface PageSnapedListener {

		/**
		 * @mcoy
		 * 当从某一页滑动到另一页完成时的回调函数
		 */
		void onSnapedCompleted(int derection);
	}

       。。。。。。

	/**
	 * 设置上下页面
	 * @param pageTop
	 * @param pageBottom
	 */
	public void setSnapPages(McoySnapPage pageTop, McoySnapPage pageBottom) {
		mPageTop = pageTop;
		mPageBottom = pageBottom;
		addPagesAndRefresh();
	}

	private void addPagesAndRefresh() {
		// 设置页面id
		mPageTop.getRootView().setId(0);
		mPageBottom.getRootView().setId(1);
		addView(mPageTop.getRootView());
		addView(mPageBottom.getRootView());
		postInvalidate();
	}

	/**
	 * @mcoy add
	 * computeScroll方法会调用postInvalidate()方法, 而postInvalidate()方法中系统
	 * 又会调用computeScroll方法, 因此会一直在循环互相调用, 循环的终结点是在computeScrollOffset()
	 * 当computeScrollOffset这个方法返回false时,说明已经结束滚动。
	 *
	 * 重要:真正的实现此view的滚动是调用scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
	 */
	@Override
	public void computeScroll() {
		//先判断mScroller滚动是否完成
		if (mScroller.computeScrollOffset()) {
			if (mScroller.getCurrY() == (mScroller.getFinalY())) {
				if (mNextDataIndex > mDataIndex) {
				    mFlipDrection = FLIP_DIRECTION_DOWN;
				    makePageToNext(mNextDataIndex);
				} else if (mNextDataIndex < mDataIndex) {
				    mFlipDrection = FLIP_DIRECTION_UP;
				    makePageToPrev(mNextDataIndex);
				}else{
				    mFlipDrection = FLIP_DIRECTION_CUR;
				}
				if(mPageSnapedListener != null){
					mPageSnapedListener.onSnapedCompleted(mFlipDrection);
				}
			}
			//这里调用View的scrollTo()完成实际的滚动
			scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
			//必须调用该方法,否则不一定能看到滚动效果
			postInvalidate();
		}
	}

	private void makePageToNext(int dataIndex) {
		mDataIndex = dataIndex;
        mCurrentScreen = getCurrentScreen();
	}

	private void makePageToPrev(int dataIndex) {
		mDataIndex = dataIndex;
        mCurrentScreen = getCurrentScreen();
	}

	public int getCurrentScreen() {
		for (int i = 0; i < getChildCount(); i++) {
			if (getChildAt(i).getId() == mDataIndex) {
				return i;
			}
		}
		return mCurrentScreen;
	}

	public View getCurrentView() {
		for (int i = 0; i < getChildCount(); i++) {
			if (getChildAt(i).getId() == mDataIndex) {
				return getChildAt(i);
			}
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
	 * 重写了父类的onInterceptTouchEvent(),主要功能是在onTouchEvent()方法之前处理
	 * touch事件。包括:down、up、move事件。
	 * 当onInterceptTouchEvent()返回true时进入onTouchEvent()。
	 */
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		final int action = ev.getAction();
		if ((action == MotionEvent.ACTION_MOVE)
				&& (mTouchState != TOUCH_STATE_REST)) {
			return true;
		}
		final float x = ev.getX();
		final float y = ev.getY();

		switch (action) {
		case MotionEvent.ACTION_MOVE:
			// 记录y与mLastMotionY差值的绝对值。
            // yDiff大于gapBetweenTopAndBottom时就认为界面拖动了足够大的距离,屏幕就可以移动了。
			final int yDiff = (int)(y - mLastMotionY);
			boolean yMoved = Math.abs(yDiff) > gapBetweenTopAndBottom;
			if (yMoved) {
				if(MCOY_DEBUG) {
					Log.e(TAG, "yDiff is " + yDiff);
					Log.e(TAG, "mPageTop.isFlipToBottom() is " + mPageTop.isAtBottom());
					Log.e(TAG, "mCurrentScreen is " + mCurrentScreen);
					Log.e(TAG, "mPageBottom.isFlipToTop() is " + mPageBottom.isAtTop());
				}
				if(yDiff < 0 && mPageTop.isAtBottom() && mCurrentScreen == 0
						|| yDiff > 0 && mPageBottom.isAtTop() && mCurrentScreen == 1){
					Log.e("mcoy", "121212121212121212121212");
					mTouchState = TOUCH_STATE_SCROLLING;
				}
			}
			break;
		case MotionEvent.ACTION_DOWN:
			// Remember location of down touch
			mLastMotionY = y;
			Log.e("mcoy", "mScroller.isFinished() is " + mScroller.isFinished());
			mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
					: TOUCH_STATE_SCROLLING;
			break;
		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP:
			// Release the drag
			mTouchState = TOUCH_STATE_REST;
			break;
		}
		boolean intercept = mTouchState != TOUCH_STATE_REST;
		Log.e("mcoy", "McoySnapPageLayout---onInterceptTouchEvent return " + intercept);
		return intercept;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see android.view.View#onTouchEvent(android.view.MotionEvent)
	 * 主要功能是处理onInterceptTouchEvent()返回值为true时传递过来的touch事件
	 */
	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		Log.e("mcoy", "onTouchEvent--" + System.currentTimeMillis());
	    if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);

		final int action = ev.getAction();
		final float x = ev.getX();
		final float y = ev.getY();
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			if (!mScroller.isFinished()) {
				mScroller.abortAnimation();
			}
			break;
		case MotionEvent.ACTION_MOVE:
		    if(mTouchState != TOUCH_STATE_SCROLLING){
                 // 记录y与mLastMotionY差值的绝对值。
                 // yDiff大于gapBetweenTopAndBottom时就认为界面拖动了足够大的距离,屏幕就可以移动了。
                final int yDiff = (int) Math.abs(y - mLastMotionY);
                boolean yMoved = yDiff > gapBetweenTopAndBottom;
                if (yMoved) {
                	mTouchState = TOUCH_STATE_SCROLLING;
                }
            }
            // 手指拖动屏幕的处理
            if ((mTouchState == TOUCH_STATE_SCROLLING)) {
                // Scroll to follow the motion event
                final int deltaY = (int) (mLastMotionY - y);
                mLastMotionY = y;
                final int scrollY = getScrollY();
                if(mCurrentScreen == 0){//显示第一页,只能上拉时使用
                	if(mPageTop != null && mPageTop.isAtBottom()){
                		scrollBy(0, Math.max(-1 * scrollY, deltaY));
                	}
                }else{
                	if(mPageBottom != null && mPageBottom.isAtTop()){
                		 scrollBy(0, deltaY);
                	}
                }
            }
			break;
		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP:
			// 弹起手指后,切换屏幕的处理
			if (mTouchState == TOUCH_STATE_SCROLLING) {
			    final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int velocityY = (int) velocityTracker.getYVelocity();
                if (Math.abs(velocityY) > SNAP_VELOCITY) {
                    if( velocityY > 0 && mCurrentScreen == 1 && mPageBottom.isAtTop()){
                        snapToScreen(mDataIndex-1);
                    }else if(velocityY < 0  && mCurrentScreen == 0){
                        snapToScreen(mDataIndex+1);
                    }else{
                        snapToScreen(mDataIndex);
                    }
                } else {
                    snapToDestination();
                }
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
			}else{
			}
			mTouchState = TOUCH_STATE_REST;
			break;

		default:
			break;
		}
		return true;
	}

	private void clearOnTouchEvents(){
		mTouchState = TOUCH_STATE_REST;
		 if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
             mVelocityTracker = null;
         }
	}

    private void snapToDestination() {
		// 计算应该去哪个屏
		final int flipHeight = getHeight() / 8;

        int whichScreen = -1;
        final int topEdge = getCurrentView().getTop();

        if(topEdge < getScrollY() && (getScrollY()-topEdge) >= flipHeight && mCurrentScreen == 0){
            //向下滑动
            whichScreen = mDataIndex + 1;
        }else if(topEdge > getScrollY() && (topEdge - getScrollY()) >= flipHeight && mCurrentScreen == 1){
            //向上滑动
            whichScreen = mDataIndex - 1;
        }else{
            whichScreen = mDataIndex;
        }
        Log.e(TAG, "snapToDestination mDataIndex = " + mDataIndex);
    	Log.e(TAG, "snapToDestination whichScreen = " + whichScreen);
        snapToScreen(whichScreen);
	}

	private void snapToScreen(int dataIndex) {
        if (!mScroller.isFinished())
            return;

        final int direction = dataIndex - mDataIndex;
        mNextDataIndex = dataIndex;
        boolean changingScreens = dataIndex != mDataIndex;
        View focusedChild = getFocusedChild();
        if (focusedChild != null && changingScreens) {
            focusedChild.clearFocus();
        }
        //在这里判断是否已到目标位置~
        int newY = 0;
		switch (direction) {
		case 1:  //需要滑动到第二页
			Log.e(TAG, "the direction is 1");
			newY = getCurrentView().getBottom(); // 最终停留的位置
			break;
		case -1:  //需要滑动到第一页
			Log.e(TAG, "the direction is -1");
			Log.e(TAG, "getCurrentView().getTop() is "
					+ getCurrentView().getTop() + " getHeight() is "
					+ getHeight());
			newY = getCurrentView().getTop() - getHeight(); // 最终停留的位置
			break;
		case 0:  //滑动距离不够, 因此不造成换页,回到滑动之前的位置
			Log.e(TAG, "the direction is 0");
			newY = getCurrentView().getTop(); //第一页的top是0, 第二页的top应该是第一页的高度
			break;
		default:
			break;
		}
        final int cy = getScrollY(); // 启动的位置
        Log.e(TAG, "the newY is " + newY + " cy is " + cy);
        final int delta = newY - cy; // 滑动的距离,正值是往左滑<—,负值是往右滑—>
        mScroller.startScroll(0, cy, 0, delta, Math.abs(delta));
        invalidate();
    }

    。。。。

}

McoySnapPage是定义在VIewGroup的一个接口, 比如说我们需要类似某东商品详情那样,有上下两页的效果。 那我就需要自己定义两个类实现这个接口,并实现接口的方法。getRootView需要返回当前页需要显示的布局内容;isAtTop需要返回当前页是否已经在顶端; isAtBottom需要返回当前页是否已经在底部

onInterceptTouchEvent和onTouchEvent决定当前的滑动状态, 并决定是有当前VIewGroup拦截touch事件还是由子view去消费touch事件

Demo地址: http://download.csdn.net/detail/zxm317122667/8926295

PS: Mcoy是本人的英文名称, 希望不要引起误会^_^

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-14 20:08:52

自定义ViewGroup实现仿淘宝的商品详情页的相关文章

自己定义ViewGroup实现仿淘宝的商品详情页

近期公司在新版本号上有一个须要. 要在首页加入一个滑动效果, 详细就是仿照X宝的商品详情页, 拉到页面底部时有一个粘滞效果, 例如以下图 X东的商品详情页,假设用户继续向上拉的话就进入商品图文描写叙述界面: watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" > 刚開始是想拿来主义.直接从网上找个现成的de

自定义View之仿淘宝详情页

自定义View之仿淘宝详情页 转载请标明出处: http://blog.csdn.net/lisdye2/article/details/52353071 本文出自:[Alex_MaHao的博客] 项目中的源码已经共享到github,有需要者请移步[Alex_MaHao的github] 基本介绍 现在的一些购物类App例如淘宝,京东等,在物品详情页,都采用了类似分层的模式,即上拉加载详情的方式,节省了空间,使用户的体验更加的舒适.只要对于某个东西的介绍很多时,都可以采取这样的方式,第一个页面显示

仿淘宝京东商品图片放大预览功能

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>图片放大镜效果</title> <style> *{ padding: 0; margin: 0; } body{ padding: 50px; position: relative; } .goods { width: 220px; height: 200px; position

类似淘宝或者京东详情页的效果实现

一个scrollView,两个tableView,sc设置不能滑动,并且滑动范围是两个tableView的高度和.借助第三方的上下拉刷新,分别在两个tableView的上下拉刷新事件中,实现contentOffset的y的改变. MJRefresh是可以设置刷新控件的偏移的来控制初始是否显示刷新控件ignoredScrollViewContentInsetBottom,或者设置tableView的contentInset. kvo修改系统的箭头和刷新文字的间距 #import "MDJRefre

Android自定义控件实战——仿淘宝商品浏览界面

转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38656929 用手机淘宝浏览商品详情时,商品图片是放在后面的,在第一个ScrollView滚动到最底下时会有提示,继续拖动才能浏览图片.仿照这个效果写一个出来并不难,只要定义一个Layout管理两个ScrollView就行了,当第一个ScrollView滑到底部时,再次向上滑动进入第二个ScrollView.效果如下: 需要注意的地方是: 1.如果是手动滑到底部需要再次按下

Android中仿淘宝首页顶部滚动自定义HorizontalScrollView定时水平自动切换图片

Android中仿淘宝首页顶部滚动自定义HorizontalScrollView定时水平自动切换图片 自定义ADPager 自定义水平滚动的ScrollView效仿ViewPager 当遇到要在ViewPager中添加多张网络请求图片的情况下,不能进行复用,导致每次都要重新去求情已经请求过的数据致使流量数据过大 自定义的数据结构解决了这个问题,固定传递的图片数据之后进行统一请求,完成后进行页面切换数据复用 代码中涉及网络请求是用的Volley网络请求框架 PicCarousel是网络数据请求的U

一款仿淘宝购物的商品列表页面多条件查询(含有单选和全部)

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Typ

Ecshop 商品页配送方式添加 实现仿淘宝按地区显示运费

Ecshop实现仿淘宝按地区显示运费 淘宝网(Taobao)购物的宝贝详情页面,可以针对不同地区显示不同运费,运费由后台设定:结算时间,按重量.件数计算运费.Ecshop本身有配送方式插件,已有多家物流公司插件,例如:顺丰快递.申通快递.圆通快递等.本文介绍如何实现按地区显示运费,并且让每个商品绑定运费模板. 1.Ecshop后台配送方式创建 进入Ecshop后台"系统设置-->配送方式",将“顺丰快递”改名称为“粮食快递”,配送ID号为6. 2.商品绑定配送方式的运费模板 2.

Vue实现仿淘宝商品详情属性选择的功能

Vue实现仿淘宝商品详情属性选择的功能 2018年09月07日 17:13:35 yx_cos 阅读数:278 先看下效果图:(同个属性内部单选,属性与属性之间可以多选) 主要实现过程: 所使用到的数据类型是(一个大数组里面嵌套了另一个数组)具体格式如下: attrAndValuees: [   {   'attrId': 1,   'attrName': '味道',   'valuees': [   {   'attrId': 1,   'value': '橘子味'   },   {   'a