android 自定义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

转自:http://blog.csdn.net/zxm317122667/article/details/47018357

时间: 2024-09-15 16:25:51

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

android自定义ProgressBar(仿淘宝)的加载效果

三种方式实现自定义圆形页面加载中效果的进度条  To get a ProgressBar in the default theme that is to be used on white/light back ground, use one of the inverse styles: <ProgressBar style="@android:style/Widget.ProgressBar.Inverse"/> <ProgressBar style="@a

安卓(android)仿电商app商品详情页按钮浮动效果_Android

1.效果图如下: 这效果用户体验还是很酷炫,今天我们就来讲解如何实现这个效果. 2.分析 为了方便理解,作图分析 如图所示,整个页面分为四个部分:      1.悬浮内容,floatView      2.顶部内容,headView      3.中间内容,与悬浮内容相同,middleView      4.商品详情展示页面,detailView 因为页面内容高度会超出屏幕,所以用Scrollview实现滚动,悬浮view与scrollview同级,都在一个帧布局或者相对布局中. 当y方向的滚动

Android控件SeekBar仿淘宝滑动验证效果_Android

SeekBar是一个拖动条控件,最简单的案例就是我们的调节音量,还有音频视频的播放,传统的SeekBar样式,如图 传统的实现太简单,不足以让我们到能装逼的地步.本来是打算实现滴滴出行滑动完成订单的效果,可惜找不到效果图,今天也就用淘宝的滑动验证来作为实例 1.1 实现分析 SeekBar:使用progressDrawable属性自定义SeekBar 拖动块:使用thumb属性更改,其实就是一张图片 文字:使用RelativeLayout嵌套在一起 1.2 实现布局 <?xml version=

Android控件SeekBar仿淘宝滑动验证效果

SeekBar是一个拖动条控件,最简单的案例就是我们的调节音量,还有音频视频的播放,传统的SeekBar样式,如图 传统的实现太简单,不足以让我们到能装逼的地步.本来是打算实现滴滴出行滑动完成订单的效果,可惜找不到效果图,今天也就用淘宝的滑动验证来作为实例 1.1 实现分析 SeekBar:使用progressDrawable属性自定义SeekBar 拖动块:使用thumb属性更改,其实就是一张图片 文字:使用RelativeLayout嵌套在一起 1.2 实现布局 <?xml version=

安卓(android)仿电商app商品详情页按钮浮动效果

1.效果图如下: 这效果用户体验还是很酷炫,今天我们就来讲解如何实现这个效果. 2.分析 为了方便理解,作图分析 如图所示,整个页面分为四个部分: 1.悬浮内容,floatView 2.顶部内容,headView 3.中间内容,与悬浮内容相同,middleView 4.商品详情展示页面,detailView 因为页面内容高度会超出屏幕,所以用Scrollview实现滚动,悬浮view与scrollview同级,都在一个帧布局或者相对布局中. 当y方向的滚动距离小于中间的内容middleView到

Android实现仿淘宝购物车增加和减少商品数量功能demo示例_Android

本文实例讲述了Android实现仿淘宝购物车增加和减少商品数量功能.分享给大家供大家参考,具体如下: 在前面一篇<Android实现的仿淘宝购物车demo示例>中,小编简单的介绍了如何使用listview来实现购物车,但是仅仅是简单的实现了列表的功能,随之而来一个新的问题,买商品的时候,我们可能不止想买一件商品,想买多个,或许有因为某种原因点错了,本来想买一件来着,小手不小心抖了一下,把数量错点成了三个,这个时候就涉及到一个新的功能,那就是增加和减少商品的数量,今天这篇博文,小编就来和小伙伴们

Android仿淘宝商品浏览界面图片滚动效果_Android

用手机淘宝浏览商品详情时,商品图片是放在后面的,在第一个ScrollView滚动到最底下时会有提示,继续拖动才能浏览图片.仿照这个效果写一个出来并不难,只要定义一个Layout管理两个ScrollView就行了,当第一个ScrollView滑到底部时,再次向上滑动进入第二个ScrollView.效果如下: 需要注意的地方是:       1.如果是手动滑到底部需要再次按下才能继续往下滑,自动滚动到底部则不需要       2.在由上一个ScrollView滑动到下一个ScrollView的过程中

jQuery仿淘宝网产品品牌隐藏与显示效果_jquery

本文实例讲述了jQuery仿淘宝网产品品牌隐藏与显示效果.分享给大家供大家参考.具体如下: 这里演示jQuery实现产品品牌隐藏与显示效果,仿淘宝网商品列表的品牌显示与折叠功能,很实用的代码,看了就知道了. 运行效果截图如下: 在线演示地址如下: http://demo.jb51.net/js/2015/jquery-f-taobao-product-hidden-show-codes/ 具体代码如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.

Android 仿淘宝、京东商品详情页向上拖动查看图文详情控件DEMO详解_Android

一.淘宝商品详情页效果 我们的效果 二.实现思路      使用两个scrollView,两个scrollView 竖直排列,通过自定义viewGroup来控制两个scrollView的竖直排列,以及滑动事件的处理.如下图 三.具体实现 1.继承viewGroup自定义布局View 重写onMeasure()和onLayout方法,在onLayout方法中完成对两个子ScrollView的竖直排列布局,代码如下: 布局文件: <RelativeLayout xmlns:android="h