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需要返回当前页是否已经在底部

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

Demo地址

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android仿淘宝商品详情页效果

    本文实例为大家分享了Android仿淘宝商品详情页的具体代码,供大家参考,具体内容如下 Demo地址:先上效果图 效果就是上面图片的效果 接下来看看如何实现 首先我们来看下布局文件 <LinearLayout android:id="@+id/header" android:layout_width="match_parent" android:layout_height="72dp" android:paddingTop="24

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

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

  • Android仿淘宝商品详情页

    看到有人在问如何实现淘宝商品详情页效果,献上效果图 大致梳理一下思路,这里不提供源码 状态栏透明使用开源库StatusBarCompat,为了兼容手机4.4 dependencies { compile ('com.github.niorgai:StatusBarCompat:2.1.4', { exclude group: 'com.android.support' }) } allprojects { repositories { ... maven { url "https://jitpa

  • Android自定义LinearLayout实现淘宝详情页

    1.简单说明 淘宝详情页就不用我一一介绍了,昨天逛淘宝看到这个效果时,让我想起了去年刚学习Android只会使用现成的时候,当时在网上找了一个这种效果的使用了,并不懂怎么实现的.现在就看到一种效果就想自己实现一下,我想这就是刚接触某个知识时的好奇心吧 说走咱就走啊,本文只是介绍一种实现思路,网上也已经有了很多种实现方式,有问题请指正 效果图(我有很用心的找美女图的) 2.实现思路 继承LinearLayout,设置方向为垂直 控件中有两个ScrollView,至于为什么要使用ScrollView

  • Android仿淘宝详情页面viewPager滑动到最后一张图片跳转的功能

    需要做一个仿淘宝客户端ViewPager滑动到最后一页,再拖动的时候跳到详情的功能,刚开始没什么思路,后来搜了一下,发现有好几种实现方法,最好的一种就是在ViewPager图片的后面再加一个view,然后滑动viewpager的时候,判断一下就行了. 附一个链接,我写的代码就是参考的这个,稍微改了一点点,先看看效果图. 实现起来比较简单,先写一个滑动加载详情的布局,然后在viewpager的instantiateItem里面判断一下,如果是最后一张,就显示加载详情的那个布局.不过需要注意的是,v

  • Android自定义ViewGroup实现淘宝商品详情页

    最近公司在新版本上有一个需要,要在首页添加一个滑动效果,具体就是仿照X宝的商品详情页,拉到页面底部时有一个粘滞效果,如下图X东的商品详情页,如果用户继续向上拉的话就进入商品图文描述界面: 刚开始是想拿来主义,直接从网上找个现成的demo来用, 但是网上无一例外的答案都特别统一: 几乎全部是ScrollView中再套两个ScrollView,或者是一个LinearLayout中套两个ScrollView. 通过指定父view和子view的focus来切换滑动的处理界面---即通过view的requ

  • python爬取淘宝商品详情页数据

    在讲爬取淘宝详情页数据之前,先来介绍一款 Chrome 插件:Toggle JavaScript (它可以选择让网页是否显示 js 动态加载的内容),如下图所示: 当这个插件处于关闭状态时,待爬取的页面显示的数据如下: 当这个插件处于打开状态时,待爬取的页面显示的数据如下:   可以看到,页面上很多数据都不显示了,比如商品价格变成了划线价格,而且累计评论也变成了0,说明这些数据都是动态加载的,以下演示真实价格的找法(评论内容找法类似),首先检查页面元素,然后点击Network选项卡,刷新页面,可

  • videojs+swiper实现淘宝商品详情轮播图

    本文实例为大家分享了videojs+swiper实现淘宝商品详情轮播图的具体代码,供大家参考,具体内容如下 这个引用了videojs和swiper.实现效果类似淘宝商品详情中的轮播图,首张轮播为视频: 当视频开始播放时,轮播停止.视频暂停时,轮播继续. 当视频播放中,滑动到下个slide时,视频暂停播放. swiper手册 HTML部分: <!-- swiper轮播 --> <div class="swiper-container"> <div class

  • Android自定义ViewGroup实现右滑进入详情

    目录 前言 一.抖音直接右滑进入详情 二.闲鱼右滑进入详情 三.列表的右滑进入详情 后记 前言 在之前的 ViewGroup 的事件相关一文中,我们详细的讲解了一些常见的 ViewGroup 需要处理的事件与运动的方式. 我们了解了如何处理拦截事件,如何滚动,如何处理子 View 的协调运动等. 再复杂一点,我们可以组合在一起使用.例如在拦截事件之后滚动,或者在滚动到一个阈值之后拦截事件. 今天我们一起再巩固一下相关的知识点,以比较常见的一个应用场景,右滑进入详情的场景为例子. 这个例子中又分几

  • Android自定义view仿淘宝快递物流信息时间轴

    学了Android有一段时间了,一直没有时间写博客,趁着周末有点空,就把自己做的一些东西写下来. 一方面锻炼一下自己的写文档的能力,另一方面分享代码的同时也希望能与大家交流一下技术,共同学习,共同进步. 废话不多少说,我们先来看看我们自定义view要实现的效果: 效果图 自定义属性 <resources> <declare-styleable name="TimeLineView"> <attr name="timelineRadius"

  • Android自定义EditText实现淘宝登录功能

    本文主要是自定义了EditText,当EditText有文本输入的时候会出现删除图标,点击删除图标实现文本的清空,其次对密码的返回做了处理,用*替代系统提供的.. 首先看效果图: 整体布局UI: <com.example.zdyedittext.ClearEditText android:id="@+id/editText1" android:layout_width="fill_parent" android:layout_height="35dp

随机推荐