自定义View之仿淘宝详情页

自定义View之仿淘宝详情页

转载请标明出处:

http://blog.csdn.net/lisdye2/article/details/52353071

本文出自:【Alex_MaHao的博客】

项目中的源码已经共享到github,有需要者请移步【Alex_MaHao的github】

基本介绍

现在的一些购物类App例如淘宝,京东等,在物品详情页,都采用了类似分层的模式,即上拉加载详情的方式,节省了空间,使用户的体验更加的舒适。只要对于某个东西的介绍很多时,都可以采取这样的方式,第一个页面显示必要的,第二个页面显示详细信息。

在之前写项目的时候,曾经写过一个类似淘宝详情页的UI效果,如下:

仔细分析这种UI效果,其实很简单,就是两个页面,垂直摆放,同时两个页面之间过渡时,加上一层特殊处理,及当第二个页面显示多少时,松开手指时复原或者直接显示第二个页面。

根据这种特殊的UI效果进行实现封装,最终的效果如下:

能够实现页面的切换,当滑动到第二个页面不足显示区域的三分之一时,则松开手指时会还原,如果超过三分之一,则会跳到第二个页面。

同时实现了一些事件分发的处理,能够嵌入按钮,ViewPager等控件。

使用方式

编写xml文件,添加控件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.mahao.alex.customviewdemo.taobao.DetailVerticalView
        android:id="@+id/detailVerticalView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="1000dp"
            android:background="#f0f">

            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="test" />
        </LinearLayout>

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="1000dp"
            android:background="#ff0" />

    </com.mahao.alex.customviewdemo.taobao.DetailVerticalView>

</RelativeLayout>

在实现中,一定要将控件的高度设置为match_parent,因为在代码中需要获取显示区域的高度,用以判断是否复原和跳转页面。

在该控件中添加两个布局控件。

Activity中查找控件并设置一些监听。

public class DetailVerticalActivity extends AppCompatActivity {

    private DetailVerticalView v;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_taobao_detail);
        // 查找控件
        v = (DetailVerticalView) findViewById(R.id.detailVerticalView);
        // 设置滚动监听的回调
        v.setScrollChangeListener(new DetailVerticalView.ScrollChangeListener() {

            @Override
            public void scrollY(int y) {
                // y轴滑动偏移量的回调
            }

            @Override
            public void onScollStateChange(int type) {
                // 滑动到那个页面状体的变化
                if(DetailVerticalView.TOP == type){
                    Toast.makeText(DetailVerticalActivity.this, "1", Toast.LENGTH_SHORT).show();
                }else if(DetailVerticalView.BOTTOM==type){
                    Toast.makeText(DetailVerticalActivity.this, "2", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }

    public void test(View view){
        Toast.makeText(getApplicationContext(),"点击",Toast.LENGTH_SHORT).show();
    }
}

实现原理

在实现过程中,主要考虑以下几点问题:

  • 如何布置两个页面的控件位置。

解决方式是使自定义View实现ViewGroup,重写onMeasureonLayout方法,对子View进行布局。

  • 如何判断两个页面何时跳转,

解决方式:获取View显示区域高度的三分之一,如果超过,则显示第二个页面,否则

则恢复原状,不显示第二个页面。

  • 如何实现滑动的过渡。

解决方式:使用Scroller进行滑动的控制。

  • 判断的一些边界的问题。

滑动到底部和滑动到顶部时,等不可再滑,需要对边界进行配置。

  • 事件分发的处理。

对于点击等事件,利用onInterceptTouchEvent() 对相关事件进行拦截。

代码实现

创建对象,初始化控件

/**
 *
 * 仿淘宝详情页
 * Created by alex_mahao on 2016/8/29.
 */
public class DetailVerticalView extends ViewGroup {

    // 滑动到顶部的状态
    public static final int TOP = 1;

    // 滑动到底部的状态
    public static final int BOTTOM = 2;

    public static final String TAG = "DetailVerticalView";

    /**
     * 屏幕高度
     */
    private int mScreenHeight;

    /**
     * 手指上次触摸事件的y轴位置
     */
    private int mLastY;

    /**
     * 点击时y轴的偏移量
     */
    private int mScrollY;

    /**
     * 滚动控件
     */
    private Scroller mScroller;

    /**
     * 最小移动距离判定
     */
    private int mTouchSlop;

    /**
     * 滑动结束的偏移量
     */
    private int mEnd;

    /**
     * 初始按下y轴坐标
     */
    private int mDownStartY;

    /**
     * 记录当前y轴坐标
     */
    private int y;

    /**
     * 控件的高度
     */
    private int mHeight;

    /**
     * 监听的回调
     */
    private ScrollChangeListener scrollChangeListener;

    public DetailVerticalView(Context context) {
        super(context, null);
    }

    public DetailVerticalView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 初始化方法
     */
    private void init() {

        // 创建滑对象,以便滑动时使用
        mScroller = new Scroller(getContext());

        // 获取系统的最小距离
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(ViewConfiguration.get(getContext()));
    }

    //....
}

一些变量的定义后面会有,此处不提。

mTouchSlop,系统默认的最小距离。当手指滑动的大小大于该值时,则认为是滑动,不在是点击,后面会通过与该值比对进行事件拦截。

测量自身的大小和两个页面控件的大小

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 获取显示区域的高度
        mScreenHeight = MeasureSpec.getSize(heightMeasureSpec);

        // 遍历子View
        int count = getChildCount();
        // 控件的高度
        mHeight = 0;
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            // 测量子View 高度
            int childHeight = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
            measureChild(childView, widthMeasureSpec, childHeight);
            mHeight = getChildAt(i).getMeasuredHeight() + mHeight;
        }
        // 设置控件的高度
        setMeasuredDimension(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));

    }

设置自定义View的高度为match_parent,就是为了获取显示区域的高度,从此处可以看出。

测量子View的高度,便于后面的布局使用。在中间,会看到我对childView的高度设置了值childHeight,该值的目的是告诉子View,我给你一个很大的值,你看着自己需要多少自己设置就行,及AT_MOST模式

在最后的时候,设置当前控件的高度,为两个页面控件的高度之和。

对两个页面控件进行布置onLayout()方法


@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int childCount = getChildCount();
        int childHeight = 0;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                // 确定位置
                child.layout(l, childHeight, r, childHeight + child.getMeasuredHeight());
                childHeight = +child.getMeasuredHeight();
            }
        }

    }

这一段没什么难度,看着理解即可。

处理事件分发

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 获取当前触摸位置Y轴坐标
        y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 第一次按下时的Y轴坐标
                mDownStartY = (int) ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                // 如果大于mTouchSlop,认为是滑动,则不再让子View处理事件
                if (Math.abs(y - mDownStartY) > mTouchSlop) {
                    // 因为是在onTouchEvent中处理,如果子View处理过事件,
                    // 则该控件的onTouchEvent()不再有DOWN事件,在这里获取一些值
                    mLastY = y;
                    mScrollY = getScrollY();
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return false;
    }

对于事件的传递,流程为 父:onInterceptTouchEventr - > 子:onTouchEvent() -> 父:onTouchEvent(),当然这里只是写了必要的流程。如果子:onTouchEvent()返回true,则当前控件就无法捕获触摸事件,那么滑动就无从处理了。所以,在此处,我们判断垂直滑动大于最小滑动距离时,就把事件给截断,不在交给子控件处理。

同时,如果子控件处理了一些事件,那么父控件的onTouchEvent()中,将不在有DOWN事件,那么我们需要先获取一次值,在打断子View的时候。

滑动相关的处理

@Override
    public boolean onTouchEvent(MotionEvent event) {
        y = (int) event.getY();
        mScrollY = getScrollY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 如果通过事件拦截获取到的触摸,则直接就为Move方法
                mLastY = y;
                mScrollY = getScrollY();
                break;

            case MotionEvent.ACTION_MOVE:
                int dy = mLastY - y;

                Log.i(TAG,"dy:"+dy+"-"+"mLastY:"+mLastY+"-"+"mScrolly:"+mScrollY);

                if(mScrollY+dy<0){
                    // 滑动到顶部,不可再滑动
                    scrollTo(0,0);
                }else if(mScrollY+dy>getMeasuredHeight()-mScreenHeight){
                    //底部时
                    scrollTo(0,getMeasuredHeight()-mScreenHeight);
                }else{
                    scrollBy(0, dy);
                    mLastY = y;
                }

                break;
            case MotionEvent.ACTION_UP:

                // 当前偏移量是
                int absScroll = mScrollY+mScreenHeight-getChildAt(0).getMeasuredHeight();

                if(absScroll<0||absScroll>mScreenHeight){
                    // 第一个页面未滑到底部,不做操作,如果absScroll>屏幕的高度,则完全滚动
                    // 不做任何滚动操作
                }else if(absScroll>mScreenHeight/3){
                    // 监听的回调
                    if(scrollChangeListener!=null){
                        scrollChangeListener.onScollStateChange(BOTTOM);
                    }
                    // 松开时显示第二个页面
                    mScroller.startScroll(0, mScrollY, 0, mScreenHeight-absScroll);
                }else if(absScroll<mScreenHeight/3){
                    // 回到第一个页面
                    if(scrollChangeListener!=null){
                        scrollChangeListener.onScollStateChange(TOP);
                    }
                    mScroller.startScroll(0,mScrollY,0,-absScroll);
                }
                break;
        }
        postInvalidate();

        return true;
    }

该段是整个自定义View中最重要的方法,总结来说干了两件事情:

  • ACTION_MOVE中,处理触摸滑动的事件,及手指在屏幕滑动时的相关处理。

边界处理,判断滑动时,如果到顶部和底部,则不再滑动,否则进行相关的滑动。

  • ACTION_UP:手指离开屏幕时,对是否需要跳转进行判断,如果需要跳转,则跳转。

在手指离开时,判断当前页面的显示量,偏移量+显示区域的高度-第一个控件的高度=第二个控件显示的高度。

如果显示的高度小于0,则表示还在第一个页面,第二个页面显示都没显示,不做任何处理。

如果显示的高度大于显示区域的高度,则表示第二个页面完全显示了,不做任何处理。

否则,如果显示的高度大于显示区域的1/3,则跳转到第二个页面,小于,则恢复到第一个页面。

可以看到通过mScroller.startScroll()实现页面的滑动跳转,使用该方式,需要重写另一个方法


   @Override
    public void computeScroll() {
        super.computeScroll();
        //判断scroller滚动是否完成
        if (mScroller.computeScrollOffset()) {
            // 这里调用View的scrollTo()完成实际的滚动
            scrollTo(0, mScroller.getCurrY());
            //刷新试图
            postInvalidate();
        }
    }

最后一步,设置一些必要的监听回调,和辅助方法

  /**
     * 监听的接口定义
     */
    public interface  ScrollChangeListener{
        void scrollY(int y);
        void onScollStateChange(int type);
    }

    /**
     * 设置监听
     * @param scrollChangeListener
     */
    public void setScrollChangeListener(ScrollChangeListener scrollChangeListener) {
        this.scrollChangeListener = scrollChangeListener;
    }
    /**
     * 回到第一个页面的顶部
     */
    public void scrollToTop(){
        mScroller.startScroll(0, getScrollY(), 0, -getScrollY());
        scrollChangeListener.onScollStateChange(TOP);
        postInvalidate();

    }
时间: 2024-10-14 18:55:36

自定义View之仿淘宝详情页的相关文章

JS仿淘宝详情页菜单条智能定位效果

类似于淘宝详情页菜单条智能定位 对于每个人来说并不陌生!如下截图所示:红色框的那部分! 基本原理: 是用JS侦听滚动事件,当页面的滚动距离(页面滚动的高度)大于或者等于 "对象"(要滚动的对象)距离页面顶部的高度,也就是说滚动的对象与窗口的上边缘接触时,立即将对象定位属性position值改成fixed(固定) (除IE6以外,因为IE6不支持fixed).对于IE6用绝对定位position:absolute,top:就是"游览器滚动的top".在 IE6下浏览看

自定义ViewGroup实现仿淘宝的商品详情页

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

仿淘宝详情转场(iOS,安卓没有这功能)

由于公司是做跨境电商的,所以对各大电商APP都有关注,最近看到淘宝iOS端(安卓没有)在商品详情点击加入购物车有一个动画效果特别赞,正好今天新版本上线,下午就抽了些时间研究了下. 主要思路是自定义转场,在弹出选择尺寸时候,对fromVC翻转,缩放等动作. 效果: code: http://files.cnblogs.com/files/10-19-92/CustomTransition.zip

自定义View实现 “手机淘宝”物流进程模块进度告知UI横向版

转载请注明出处:王亟亟的大牛之路 话不多说,先洗脑,安利!!!https://github.com/ddwhan0123/Useful-Open-Source-Android 旅游都在更啊!! 这些天都在浪几乎没撸代码,然后今天下午找了个下午茶时间捯饬了个自定义View来实现 很多APP都有却没怎么公开的一个"进度通知的View" 实现power by:https://dribbble.com/LeslyPyram 先上下原设计: 用圆+线条+颜色的变化来告知用户你现在的物件到哪了(这

高仿淘宝和聚美优品商城详情页实现《IT蓝豹》

android-vertical-slide-view高仿淘宝和聚美优品商城详情页实现,在商品详情页,向上拖动时,可以加载下一页.使用ViewDragHelper,滑动比较流畅. scrollView滑动到底部的时候,再行向上拖动时,添加了一些阻力.本项目来源:https://github.com/xmuSistone/android-vertical-slide-view主要代码如下:首先先看一下布局:  <com.stone.verticalslide.DragLayout        a

Android中仿淘宝首页顶部滚动自定义HorizontalScrollView定时水平自动切换图片

Android中仿淘宝首页顶部滚动自定义HorizontalScrollView定时水平自动切换图片 自定义ADPager 自定义水平滚动的ScrollView效仿ViewPager 当遇到要在ViewPager中添加多张网络请求图片的情况下,不能进行复用,导致每次都要重新去求情已经请求过的数据致使流量数据过大 自定义的数据结构解决了这个问题,固定传递的图片数据之后进行统一请求,完成后进行页面切换数据复用 代码中涉及网络请求是用的Volley网络请求框架 PicCarousel是网络数据请求的U

转::iOS 仿淘宝,上拉进入详情页面

今天做的主要是一个模仿淘宝,上拉进入商品详情的功能,主要是通过 tableView 与 webView 一起来实现的,当然也可根据自己的需要把 webView 替换成你想要的 1 // 2 // ViewController.m 3 // 仿淘宝,上拉进入详情 4 // 5 // Created by Amydom on 16/11/22. 6 // Copyright ? 2016年 Amydom. All rights reserved. 7 // 8 9 #import "ViewCont

Ecshop 商品页配送方式添加 实现仿淘宝按地区显示运费

Ecshop实现仿淘宝按地区显示运费 淘宝网(Taobao)购物的宝贝详情页面,可以针对不同地区显示不同运费,运费由后台设定:结算时间,按重量.件数计算运费.Ecshop本身有配送方式插件,已有多家物流公司插件,例如:顺丰快递.申通快递.圆通快递等.本文介绍如何实现按地区显示运费,并且让每个商品绑定运费模板. 1.Ecshop后台配送方式创建 进入Ecshop后台"系统设置-->配送方式",将“顺丰快递”改名称为“粮食快递”,配送ID号为6. 2.商品绑定配送方式的运费模板 2.

Vue实现仿淘宝商品详情属性选择的功能

Vue实现仿淘宝商品详情属性选择的功能 2018年09月07日 17:13:35 yx_cos 阅读数:278 先看下效果图:(同个属性内部单选,属性与属性之间可以多选) 主要实现过程: 所使用到的数据类型是(一个大数组里面嵌套了另一个数组)具体格式如下: attrAndValuees: [   {   'attrId': 1,   'attrName': '味道',   'valuees': [   {   'attrId': 1,   'value': '橘子味'   },   {   'a