仿淘宝、京东拖拽商品详情(可嵌套ViewPager、ListView、WebView、FragmentTabhost)

简介: 仿淘宝、京东拖拽商品详情(可嵌套ViewPager、ListView、WebView、FragmentTabhost)

对于电商App,商品详情无疑是很重要的一个模块,观察主流购物App的详情界面,发现大部分都是做成了上下两部分,上面展示商品规格信息,下面是H5商品详情,或者是嵌套了一个包含H5详情及评论列表的ViewPager界面,本文就是实现了一个兼容不同需求的上下滚动黏滞View-DragScrollDetailsLayout


实现效果图


首先看一下实现效果图


简单的ScrollView+Webview


当然,如果将Webview替换成其他的ListView之类的也是支持的。

image.png

scrollview+webview.gif


ScrollView+ViewPager


适用场景:底部需要添加多个界面,并且需要滑动

image.png


ScrollView+Fragmenttabhost


适用场景:底部需要添加多个界面,但是不需要滑动

image.png


实现


对于这个需求的场景,很容易想到可以分成上下两部分来实现,只需要一个Vertical的LinearLayout,其余的就是处理滚动及动画的问题,首先自定义ViewGroup内部先声明两个顶层子ViewmUpstairsView、 View mDownstairsView,并且采用一个变量CurrentTargetIndex标记当前处于操作那个View,

    public class DragScrollDetailsLayout extends LinearLayout {
        private View mUpstairsView;
        private View mDownstairsView;
        private View mCurrentTargetView;
        public enum CurrentTargetIndex {
            UPSTAIRS,
            DOWNSTAIRS;
            public static CurrentTargetIndex valueOf(int index) {
                return 1 == index ? DOWNSTAIRS : UPSTAIRS;
            }
        }

然后集中处理滚动事件,对于滚动与动画主要有如下几个问题需要解决:


  • 如何知道上面或者下面的View已经滚动的到顶部或者底部
  • 滚动到边界时,如何拦截处理滑动
  • 松手后如何处理后续的动效


如何判断滚动边界


首先来看第一个问题,如何知道上面或者下面的View滚动到了边界,其实Android源码中有个类ViewCompat,它有个函数canScrollVertically(View view, int offSet, MotionEvent ev)就可以判断当前View是否可以向哪个方向滚动,offset的正负值用来判断向上还是向下,当然,仅仅靠这个函数还是不够的,因为ViewGroup是可以相互嵌套的,也许ViewGroup本身不能滚动,但是其内部的子View却可以滚动,这时候,就需要递归遍历相关的View,比如对于ViewPager中嵌套了包含WebView或者List的Fragment。不过,并非所有的子View都需要遍历,只有与TouchEvent相关的View才需要判断。因此还需要写个函数判断View是否在TouchEvent所在的区域,如下函数isTransformedTouchPointInView:


/***
 * 判断MotionEvent是否处于View上面
 */
protected boolean isTransformedTouchPointInView(MotionEvent ev, View view) {
    float x = ev.getRawX();
    float y = ev.getRawY();
    int[] rect = new int[2];
    view.getLocationInWindow(rect);
    float localX = x - rect[0];
    float localY = y - rect[1];
    return localX >= 0 && localX < (view.getRight() - view.getLeft())
            && localY >= 0 && localY < (view.getBottom() - view.getTop());
}

之后我们可以利用该函数对View进行递归遍历,判断最上层的ViewGroup是否可以上下滑动

private boolean canScrollVertically(View view, int offSet, MotionEvent ev) {
      if (!mChildHasScrolled && !isTransformedTouchPointInView(ev, view)) {
            return false;
        }
        if (ViewCompat.canScrollVertically(view, offSet)) {
            mChildHasScrolled = true;
            return true;
        }
        if (view instanceof ViewPager) {
            return canViewPagerScrollVertically((ViewPager) view, offSet, ev);
        }
        if (view instanceof ViewGroup) {
            ViewGroup vGroup = (ViewGroup) view;
            for (int i = 0; i < vGroup.getChildCount(); i++) {
                if (canScrollVertically(vGroup.getChildAt(i), offSet, ev)) {
                    mChildHasScrolled = true;
                    return true;
                }
            }
        }
        return false;
    }

知道View是否可以上下滑动到边界后,拦截事件的时机就比较清晰了,那么接着看第二个问题,如何拦截滑动。


事件拦截处理


onInterceptTouchEvent在返回True之后,就不会再执行了,我们只需要把握准确的拦截时机,比如如果处于上面的View,就要对上拉事件比较敏感,处于底部就要对下拉事件敏感,同时还要将无效的手势归零,比如,操作上面的View时,如果先是下拉,并且是无效的下拉,那么就要将拦截点重置。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            mDownMotionX = ev.getX();
            mDownMotionY = ev.getY();
            if (mVelocityTracker == null) {
                mVelocityTracker = VelocityTracker.obtain();
            }
            mVelocityTracker.clear();
            mChildHasScrolled=false;
            break;
        case MotionEvent.ACTION_MOVE:
            adjustValidDownPoint(ev);
            return checkCanInterceptTouchEvent(ev);
        default:
            break;
    }
    return false;
}

checkCanInterceptTouchEvent主要用来判断是否需要拦截,并非不可滚动,就需要拦截事件,不可滚动只是一个必要条件而已,

   private boolean checkCanInterceptTouchEvent(MotionEvent ev) {
   final float xDiff = ev.getX() - mDownMotionX;
   final float yDiff = ev.getY() - mDownMotionY;
   if (!canChildScrollVertically((int) yDiff,ev)) {
       mInitialInterceptY = (int) ev.getY();
       if (Math.abs(yDiff) > mTouchSlop && Math.abs(yDiff) >= Math.abs(xDiff)
               && !(mCurrentViewIndex == CurrentTargetIndex.UPSTAIRS && yDiff > 0
               || mCurrentViewIndex == CurrentTargetIndex.DOWNSTAIRS && yDiff < 0)) {
           return true;
       }
   }
   return false;
}   

事件拦截之后,就是对Move事件进行处理

@Override
public boolean onTouchEvent(MotionEvent ev) {
    switch (ev.getActionMasked()) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            flingToFinishScroll();
            recycleVelocityTracker();
            break;
        case MotionEvent.ACTION_MOVE:
            scroll(ev);
            break;
        default:
            break;
    }
    return true;
}

滚动比较简单,直接调用scrollTo就可以,同时为了收集滚动速度,还可以用VelocityTracker做一下记录:

private void scroll(MotionEvent event) {
    if (mCurrentViewIndex == CurrentTargetIndex.UPSTAIRS) {
        if (getScrollY() <= 0 && event.getY() > mInitialInterceptY) {
            mInitialInterceptY = (int) event.getY();
        }
        scrollTo(0, (int) (mInitialInterceptY - event.getY()));
    } else {
        if (getScrollY() >= mUpstairsView.getMeasuredHeight() && event.getY() < mInitialInterceptY) {
            mInitialInterceptY = (int) event.getY();
        }
        scrollTo(0, (int) (mInitialInterceptY - event.getY() + mUpstairsView.getMeasuredHeight()));
    }
    mVelocityTracker.addMovement(event);
}


收尾动画


在Up事件之后,还要简单的处理一下一下收尾的滚动动画,比如,滚动距离不够要复原,否则,就滚动到目标视图,这里主要是根据Up事件的位置,计算需要滚动的距离,并通过Scroller来完成剩下的滚动。

private void flingToFinishScroll() {
    final int pHeight = mUpstairsView.getMeasuredHeight();
    final int threshold = (int) (pHeight * mPercent);
    float scrollY = getScrollY();
    if (CurrentTargetIndex.UPSTAIRS == mCurrentViewIndex) {
        if (scrollY <= 0) {
            scrollY = 0;
        } else if (scrollY <= threshold) {
            if (needFlingToToggleView()) {
                scrollY = pHeight - getScrollY();
                mCurrentViewIndex = CurrentTargetIndex.DOWNSTAIRS;
            } else
                scrollY = -getScrollY();
        } else {
            scrollY = pHeight - getScrollY();
            mCurrentViewIndex = CurrentTargetIndex.DOWNSTAIRS;
        }
    } else if (CurrentTargetIndex.DOWNSTAIRS == mCurrentViewIndex) {
        if (pHeight - scrollY <= threshold) {
            if (needFlingToToggleView()) {
                scrollY = -getScrollY();
                mCurrentViewIndex = CurrentTargetIndex.UPSTAIRS;
            } else
                scrollY = pHeight - scrollY;
        } else if (scrollY < pHeight) {
            scrollY = -getScrollY();
            mCurrentViewIndex = CurrentTargetIndex.UPSTAIRS;
        }
    }
    mScroller.startScroll(0, getScrollY(), 0, (int) scrollY, mDuration);
    if (mOnSlideDetailsListener != null) {
        mOnSlideDetailsListener.onStatueChanged(mCurrentViewIndex);
    }
    postInvalidate();
}

以上就是常用商品详情黏滞布局的实现。


目录
相关文章
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
1216 4
|
前端开发 小程序
前端解析支付宝返回form表单,自动跳转支付
前端解析支付宝返回form表单,自动跳转支付
988 1
|
JavaScript CDN
js:spark-md5分片计算文件的md5值
js:spark-md5分片计算文件的md5值
1993 0
|
12月前
|
API 网络架构
一文带你了解 Flutter 路由
一文带你了解 Flutter 路由
406 5
|
Shell Linux 网络安全
Linux中用户和个人配置文件介绍:讲解如何查看和修改Linux系统中的用户和个人配置文件
Linux中用户和个人配置文件介绍:讲解如何查看和修改Linux系统中的用户和个人配置文件
522 1
|
SQL 分布式计算 Ubuntu
【Hive】Hive开启远程连接及访问方法
【Hive】Hive开启远程连接及访问方法
4348 0
|
算法 数据可视化 Java
JAVA规则引擎工具有哪些?
本文对比分析了六种Java规则引擎:Drools、IBM ODM (JRules)、Easy Rules、JBPM、OpenL Tablets以及Apache Camel结合规则组件的应用。Drools是一款功能全面的业务规则管理系统,支持DRL文件定义规则、高效的规则匹配算法、复杂的规则流及决策表,并易于与Java应用集成。IBM ODM (原JRules)提供了强大的规则管理功能,包括Web界面和Eclipse插件定义管理规则、直观的决策表和决策树、REST和Java API集成选项及优化的性能。
2305 3
|
JSON API PHP
如何使用PHP开发API接口?
本文详细介绍了如何使用PHP开发API接口,涵盖从基础概念到实战步骤的全过程。首先解释了API接口的基本原理,包括HTTP协议、REST架构风格、JSON格式和OAuth认证机制。接着介绍了开发环境的设置,包括PHP安装、Web服务器配置、数据库设置等。文章还探讨了API开发的完整流程,从需求确定、框架选择、端点设计到代码编写、测试、安全性考量及性能优化。最后通过一个实战案例演示了如何创建一个简单的API端点,并讨论了部署与监控的方法。
588 0
|
JavaScript 前端开发 Android开发
Flutter笔记:关于WebView插件的用法(下)
Flutter笔记:关于WebView插件的用法(下)
1071 5
|
存储 缓存 前端开发
【Flutter前端技术开发专栏】Flutter中的图片加载与缓存优化
【4月更文挑战第30天】本文探讨了 Flutter 中如何优化图片加载与缓存,以提升移动应用性能。通过使用图片占位符、压缩裁剪、缓存策略(如`cached_network_image`插件)以及异步加载和预加载图片,可以显著加快加载速度。此外,利用`FadeInImage`、`FutureBuilder`和图片库等工具,能进一步改善用户体验。优化图片处理是提升Flutter应用效率的关键,本文为开发者提供了实用指导。
1272 0
【Flutter前端技术开发专栏】Flutter中的图片加载与缓存优化