自己定义ViewGroup实现仿淘宝的商品详情页
近期公司在新版本号上有一个须要。 要在首页加入一个滑动效果, 详细就是仿照X宝的商品详情页, 拉到页面底部时有一个粘滞效果,
例如以下图 X东的商品详情页,假设用户继续向上拉的话就进入商品图文描写叙述界面:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
刚開始是想拿来主义。直接从网上找个现成的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是本人的英文名称, 希望不要引起误会^_^
自己定义ViewGroup实现仿淘宝的商品详情页的更多相关文章
- iOS 集成阿里百川最新版(3.1.1.96) 实现淘宝授权登录以及调用淘宝客户端商品详情页
公司最近要做第三方登录,由于是做导购项目,必不可少的有淘宝的授权登录.本来就是一个授权登录,没什么大不了的.但淘宝的无线开放业务——阿里百川更新的最新版本3.1.1.96,开发文档不是不详细,是很 ...
- 使用Python 爬取 京东 ,淘宝。 商品详情页的数据。(避开了反爬虫机制)
以下是爬取京东商品详情的Python3代码,以excel存放链接的方式批量爬取.excel如下 代码如下 from selenium import webdriver from lxml import ...
- iOS开发 仿淘宝,京东商品详情3D动画
- (void)show { [[UIApplication sharedApplication].windows[0] addSubview:self.projectView]; CGRect fr ...
- Android 仿电商app商品详情页按钮浮动效果
1.效果图如下: 这效果用户体验还是很酷炫,今天我们就来讲解如何实现这个效果. 2.分析 为了方便理解,作图分析 如图所示,整个页面分为四个部分: 1.悬浮内容,floatView 2.顶部内容,he ...
- App 仿淘宝:控制详情和购买须知样式切换,控制商品详情和购买须知选项卡的位置(固定在顶部还是正常)
CSS: <div id="details" ref="details" class="details" :class="t ...
- Android点击跳转到淘宝的某一商品详情页或者某一店铺页面
最近项目的有个需求是点击购买资料按钮进入淘宝界面,简单分析一下,如果用户手机有淘宝就打开淘宝的页面,没有的话也可以选择使用webView进行展示,还是使用手机浏览器进行展示. 判断有无淘宝的代码就不贴 ...
- Android自己定义控件实战——仿淘宝商品浏览界面
转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38656929 用手机淘宝浏览商品详情时,商品图片是放在后面的,在第一个Scr ...
- Android自定义控件实战——仿淘宝商品浏览界面
转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38656929 用手机淘宝浏览商品详情时,商品图片是放在后面的,在第一个Scr ...
- Android中仿淘宝首页顶部滚动自定义HorizontalScrollView定时水平自动切换图片
Android中仿淘宝首页顶部滚动自定义HorizontalScrollView定时水平自动切换图片 自定义ADPager 自定义水平滚动的ScrollView效仿ViewPager 当遇到要在Vie ...
随机推荐
- html-字体属性
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- html-伪类
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Lunch War with the Donkey CSU - 2084
Jingze is a big figure in California State University for his stubbornness. Because of his new failu ...
- html5手势操作与多指操作封装与Canvas图片裁切实战
当前情况,移动端的开发占比越来越高,单指的拖拽触碰等操作是常规需要.特殊的多指操作与手势操作还需另做处理,而且还涉及到兼容性问题. // 屏幕上存在两根或两根以上的手指 时触发 仅IOS存在手势事件, ...
- JVM之浮点数(float)表示
1. 浮点数的组成:符号位.指数位.尾数位. 1.1 符号位: 占1位,表示正负数: 1.2 指数位: 占8位: 1.3 尾数位: 占23位. 2. 浮点数的表示: 2.1 取值: sflag * ...
- [OC]时间格式中的字符的意义
Letter Date or Time Component Presentation Examples G Era designator Text AD y Year Year 1996;96 M M ...
- python基础一 ------linux某目录下批量的为特定文件加入可执行权限
需求: 一个文件夹中有个文件,要求对特定的文件加入可执行权限 某文件系统目录下有一系列文件: quicksort graph.py heap.java install.sh ...
- git 撤销更改的文件
在没有git add之前: 1.撤销所有更改:git checkout . 2.撤销指定文件的更改:git checkout -- file.txt git add之后: git reset HEAD ...
- 利用Spring的junit4测试
利用Spring的JUnit4进行测试 不需要再显式创建Spring容器和getBean @RunWith(SpringJUnit4ClassRunner.class) @ContextConfigu ...
- cena评测系统:自定义校验器(自定义评测插件编写)
Cena评测系统,最受欢迎的信息学竞赛离线评测系统. 它是开放源程序的信息学竞赛评测系统,能满足大多数程序设计竞赛的测评需求. 特色功能: 通过局域网自动收取选手程序. 高效率的数据文件配置工具. 自 ...