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
android 自定义ViewGroup实现仿淘宝的商品详情页相关推荐
- 自己定义ViewGroup实现仿淘宝的商品详情页
近期公司在新版本号上有一个须要. 要在首页加入一个滑动效果, 详细就是仿照X宝的商品详情页, 拉到页面底部时有一个粘滞效果, 例如以下图 X东的商品详情页,假设用户继续向上拉的话就进入商品图文描写叙述 ...
- Android 仿淘宝京东商品详情页阻力翻页效果
原文链接:http://code.taobao.org/p/android-example/diff/46/trunk/%E5%95%86%E5%9F%8E%E8%AF%A6%E6%83%85/src ...
- vue 实现类似淘宝的商品详情页的商品展示
vue 实现类似淘宝的商品详情页的商品展示 利用vue和swiper实现缩略图控制 / Swiper互相控制 具体需求描述: 图片大图自动轮播展示,下面的缩略图跟着变化,点击小图切换到对应的大图,大图 ...
- 淘宝/Tmall商品详情页视频数据接口(视频数据,销量数据,sku属性数据,页面上有的数据均可以拿到,支持高并发)
Tmall商品详情页视频数据接口(视频数据,销量数据,sku属性数据,页面上有的数据均可以拿到,支持高并发)接口代码教程如下: 1.公共参数 名称 类型 必须 描述 key String 是 调用ke ...
- Android 仿淘宝京东商品详情视频+图片与图片第一帧获取
近日项目有个新需求就是把原本的商品详情只有图片展示,改为视频+图片方式展示. 此博客只提供记录,与思路具体根据自己需求实现.首先想到的是Google搜索下别人的实现方式来参考实现发现不怎么适合项目需求 ...
- 自定义View之仿淘宝详情页
自定义View之仿淘宝详情页 转载请标明出处: http://blog.csdn.net/lisdye2/article/details/52353071 本文出自:[Alex_MaHao的博客] 项 ...
- 仿淘宝京东商品规格属性选择的最简单实现
仿淘宝京东商品规格属性选择的最简单实现 商城里面的规格选择,网上大部分是自定义控件实现的,显得很是麻烦,而我的实现方式是大家最常用的控件RecyclerView,特点是性能好,简单.废话不多说,先看实 ...
- 基于BootStrap仿淘宝星星商品评价案例
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8" ...
- 仿淘宝购物车商品颜色、尺寸选择。
仿淘宝购买商品颜色.尺寸选择. 选项分为三种状态:可选.选中.不可选 上图看效果: 1,展示页 2,选中一个颜色,不支持的尺寸会自动变灰,成为不可点击状态 如果选择的是尺寸,同样颜色也可以自动筛选可以 ...
最新文章
- 干货丨史上最好记的神经网络结构速记表(经典资源,值得收藏)
- 【树莓派】小空间树莓派镜像系统备份方法img镜像文件压缩方法
- abap CA CO CS等操作符
- poj 1986 Distance Queries
- hive mysql 字符集_创建Hive表会自动更改mysql Metastore的字符集设置
- 卷王李富贵算法每日一题--分治算法(三)--逃亡
- Windows Phone 7 处理休眠和墓碑的恢复
- Silverlight C# 游戏开发:Balder的3D世界
- Jquery的ajax 三级联动 03
- C3P0连接池的配置和使用
- 计算机视觉学生论文,计算机视觉方面论文参考文献
- wpf ui分析工具 Snoop
- UVa10158 War(并查集)
- Nginx Proxy Cache原理和最佳实践
- 百行代码手撸扫雷(下)c/c++
- 修改android预览分辨率,wm命令使用方法(修改android 分辨率)修改
- 在opencv中如何生成灰度图
- Erlang 下载安装
- 将进酒翻译软件测试,乐府诗《将进酒》拼音及翻译整理
- java学生选课系统课程设计报告_Java语言程序设计课程设计-学生选课系统
热门文章
- 新闻评论如何自动生成?
- Java程序设计 实验三
- Web入门:JQuery淡入淡出面板
- 175Echarts - 象形柱图(Spirits)
- 供应&需求链的结构化管理 (zt)
- 钉钉成功了,那中国企业服务的春天还有多远?
- 现在开始 · 入门编程
- 【集成学习(上)】My_Task03掌握偏差与方差理论 笔记
- Invalid regular expression: /(.*\\__fixtures__\\.*|node_modules[\\\]react[\\...问题
- 2021-2022学年广州市海珠区九年级第一学期期中统考英语试题