PullScrollView详解(三)——PullScrollView实现

眼看下周就要休婚假了,感觉真是棒极了,嘿嘿哈哈吼吼,休假前把这个系列写完给大家

相关文章:
1、《PullScrollView详解(一)——自定义控件属性》
2、《PullScrollView详解(二)——Animation、Layout与下拉回弹》
3、《PullScrollView详解(三)——PullScrollView实现》
4、PullScrollView详解(四)——完全使用listview实现下拉回弹(方法一)
5、《PullScrollView详解(五)——完全使用listview实现下拉回弹(方法二)》
6、《PullScrollView详解(六)——延伸拓展(listview中getScrollY()一直等于0、ScrollView中的overScrollBy)》

前面铺垫的已经很多了,这篇就要进入正式环节了,下面就是这个系列最终的效果图:
这里写图片描述
下面我们就一步步来做,这篇就先跟大家一起来实现一个雏形,先把最基本的功能实现。下篇再对一些问题进行优化。
###一、框架搭建
先看看我们要搭建的框架:
这里写图片描述
大家可以看不明白,它是怎么搭的,我们还是直接上XML代码吧:
####1、XML

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/background_img"
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:layout_marginTop="-100dp"
        android:scaleType="fitXY"
        android:src="@drawable/pic3" />

    <com.harvic.pullscrollviewrudiment.PullScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
            <TableLayout
                android:id="@+id/table_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="150dp"/>
    </com.harvic.pullscrollviewrudiment.PullScrollView>
</FrameLayout>

这里要注意两个地方:
1、ImageView就是我们底部的小狗图片,它利用android:layout_marginTop="-100dp"来将图片向顶部缩进100dp,为我们下拉做准备
2、PullScrollView,派生自ScrollView,它是通过将子控件的android:layout_marginTop="150dp"来将底部的图片显示出来的。又由于,PullScrollView的android:layout_height是match_parent,所以我们可以向上滚动,覆盖住底部的图片。千万不要把android:layout_marginTop="150dp"放在PullScrollView里,比如这样:

    <com.harvic.pullscrollviewrudiment.PullScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
         android:layout_marginTop="150dp">
            <TableLayout
                android:id="@+id/table_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
    </com.harvic.pullscrollviewrudiment.PullScrollView>

这样就会有下面的效果:
这里写图片描述
这就是因为我们将android:layout_marginTop="150dp"放在了PullScrollView中;此时的PullScrollView的布局范围并不是整个屏幕了,而是从屏幕以下150dp的地方开始。
####2、PullScrollView
在这里,我们只是搭框架,并不会做什么下拉和回弹的操作,所以,我们在这一环节,我们仅仅将PullScrollView派生自ScrollView就好了
代码如下:

public class PullScrollView extends ScrollView {
    public PullScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public PullScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

####3、MainActivity
在这里其实也没什么要做的,就是把TableLayout给填充起来:

public class MainActivity extends Activity {
    private TableLayout mMainLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mMainLayout = (TableLayout) findViewById(R.id.table_layout);
        showTable();
    }


    public void showTable() {
        TableRow.LayoutParams layoutParams = new TableRow.LayoutParams(
                TableRow.LayoutParams.MATCH_PARENT,
                TableRow.LayoutParams.WRAP_CONTENT);
        layoutParams.gravity = Gravity.CENTER;
        layoutParams.leftMargin = 30;
        layoutParams.bottomMargin = 10;
        layoutParams.topMargin = 10;

        for (int i = 0; i < 30; i++) {
            TableRow tableRow = new TableRow(this);
            TextView textView = new TextView(this);
            textView.setText("Test pull down scroll view " + i);
            textView.setTextSize(20);
            textView.setPadding(15, 15, 15, 15);

            tableRow.addView(textView, layoutParams);
            if (i % 2 != 0) {
                tableRow.setBackgroundColor(Color.LTGRAY);
            } else {
                tableRow.setBackgroundColor(Color.WHITE);
            }

            mMainLayout.addView(tableRow);
        }
    }
}

核心代码就在showTable()函数中,先构造一个TableRow,在TableRow中添加一个TextView,然后将TableRow添加到TableLayout中;这里不是讲解的重点,难度也不大,所以就不再细讲了。我们用TableLayout 的主要目的就是把PullScrolLView撑开,让它可以上下滚动。
###二、下拉随手指移动
这段我们就要看看怎么让顶部的图片和上面的Content 一块随手指移动。像下面的效果:
这里写图片描述
在上篇我们讲过,让布局跟随手指移动,就是计算出手指的移动距离,然后利用layout()函数来移动布局就好了。
####1、首先有关变量的设定:

    //底部图片View
    private View mHeaderView;
    //头部图片的初始化位置
    private Rect mHeadInitRect = new Rect();
    
    //底部View
    private View mContentView;
    //contentView的初始化位置
    private Rect mContentInitRect = new Rect();
   
    //初始点击位置
    private Point mTouchPoint = new Point();    

这里主要分为三个部分:
mHeaderView:表示头部的图片的VIEW
mContentView:表示PullScrollView的子控件,这里是TableLayout控件对应的VIEW

另外是三个初始化的位置:
mTouchPoint:表示用户在滑动手指的初始点击位置。用来计算手指的移动距离的
mHeadInitRect:用来保存头部View初始化位置。用来找到回弹的位置用。
mContentInitRect:保存ContentView的初始化位置。跟mHeadInitRect一样,也是回弹用。
####2、变量初始化
首先是mHeaderView和mContentView的初始化
mContentView的初始化非常容易,与第二篇一样,我们直接在onFinishInflate()函数中,就可以获得解析后的变量实例:

protected void onFinishInflate() {
    if (getChildCount() > 0) {
        mContentView = getChildAt(0);
    }
    super.onFinishInflate();
}

但mHeaderView确是需要使用PullScrollView的用户自己传进去的,所以我们需要写一个接口来让使用PullScrollView的用户来设置它要操作的headView

public void setmHeaderView(View view){
        mHeaderView = view;
    }

同样,有关mHeadInitRect,mContentInitRect的初始化,都必须在图像显示出来以后,才能获取到头部和Content的位置,所以我们将它们的初始化全都放在当用户点击屏幕的时候,与mTouchPoint一起初始化:

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            mTouchPoint.set((int) event.getX(), (int) event.getY());
            mHeadInitRect.set(mHeaderView.getLeft(), mHeaderView.getTop(), mHeaderView.getRight(), mHeaderView.getBottom());
            mContentInitRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom());
        }
        break;
        …………
    }
}

这段代码没什么难度,就是在用户点击屏幕的时候,将headView和contentView的坐标保存起来;
####3、布局跟随手指移动
到这一步,就是要让布局跟随手指移动了。当然跟上篇一样,首先计算移动距离,然后使用layout()函数来移动布局。
计算距离:
计算距离的代码如下:

int deltaY =(int)event.getY() - mTouchPoint.y;
deltaY = deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY;

用当前的手指所在的Y轴位置减去点击时的位置。就得到了手指的移动距离:

int deltaY =(int)event.getY() - mTouchPoint.y;

但手指的移动距离并不代表是headView和contentView允许移动的距离,凡事都有一个最大值,这里假设最大值就是headview的高度。所以当手指的移动距离超过headView的高度时,我们就不再增大移动距离,而是将headView直接赋值给deltaY:

deltaY = deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY;

然后就是计算headView和contentView的应该所在位置了,并移动他们了,写出来完整的ACTION_MOVE的代码吧:

case MotionEvent.ACTION_MOVE:{
    int deltaY =(int)event.getY() - mTouchPoint.y;
    deltaY = deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY;
    if (deltaY > 0 && deltaY >= getScrollY()) {
        float headerMoveHeight = deltaY * 0.5f * SCROLL_RATIO;
        mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);
        mHeaderCurBottom = (int) (mHeadInitRect.bottom + headerMoveHeight);

        float contentMoveHeight = deltaY * SCROLL_RATIO;
        mContentTop = (int) (mContentInitRect.top + contentMoveHeight);
        mContentBottom = (int) (mContentInitRect.bottom + contentMoveHeight);

        if (mContentTop <= mHeaderCurBottom) {
            mHeaderView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, mHeaderCurBottom);
            mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, mContentBottom);
        }
    }
}
break;

先看判断语名:

if (deltaY > 0 && deltaY >= getScrollY()) {
}

deltaY > 0很容易理解,表示手指的移动距离是正值,即手指是向下移动的;
那另外一个deltaY >= getScrollY()是个什么鬼?
我们先看一下,把这个判断语句去掉,效果会怎样:(即只保留deltaY > 0)
在这里插入图片描述

看到了没,当先向上滑一段之后,再下滑,会发现,headView也会跟着下滑;这明显是不对的,因为headview应该在我们超过原始位置的时候再下滑才是正确的。所以要判断当前PullScrollView是不是已经滚动。如果手指的移动距离大于滚动距离,这才说明,我们已经超过了原始位置在下拉。这时候再移动headView;
计算高度:
使用layout移动View前,先计算当前headview和contentView应该移动的距离。

float headerMoveHeight = deltaY * 0.5f * SCROLL_RATIO;
mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);
mHeaderCurBottom = (int) (mHeadInitRect.bottom + headerMoveHeight);

float contentMoveHeight = deltaY * SCROLL_RATIO;
mContentTop = (int) (mContentInitRect.top + contentMoveHeight);
mContentBottom = (int) (mContentInitRect.bottom + contentMoveHeight);

明显,headview要比contentview移动的慢,因为我们在计算headerMoveHeight 时多乘以了一个0.5;这其实也是为了增加一个效果:让用户觉得下拉比较困难,移动的速度慢。当然你也可以去掉,也可以改成其它值。

float headerMoveHeight = deltaY * 0.5f * SCROLL_RATIO;
float contentMoveHeight = deltaY * SCROLL_RATIO;

在计算好移动高度以后,就是计算要移动的位置了,有关mHeaderCurTop、mHeaderCurBottom和mContentTop、mContentBottom的计算就不讲了,没什么意思,就是在原始高度的基础上,加上移动的高度。
移动view:
最后是使用layout移动View:

if (mContentTop <= mHeaderCurBottom) {
    mHeaderView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, mHeaderCurBottom);
    mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, mContentBottom);
    mIsMoving = true;
}

这里做了一个判断,即contentView的上边沿不能低于headView的底边。如果低于headView的底边,那就是contentView和headView分离了,这怎么能行。不能分离!
符合条件以后,就移动headview和contentview。
###三、松手回弹
在上面的那段代码中,大家可以发现,在移动headview和contentview时,设置了一个变量mIsMoving = true;这个变量是用来标识,headview和contentview是否已经被移动了位置。如果移动了位置则需要反弹;
我们上篇也讲了有关反弹的动画代码写法:先利用layout()将布局还原,然后再做动画,让它从跟随手移动的位置移动到初始位置。
先来看看代码:

case MotionEvent.ACTION_UP: {
    //反弹
    if (mIsMoving) {
        mHeaderView.layout(mHeadInitRect.left, mHeadInitRect.top, mHeadInitRect.right, mHeadInitRect.bottom);
        TranslateAnimation headAnim = new TranslateAnimation(0, 0, mHeaderCurTop - mHeadInitRect.top, 0);
        headAnim.setDuration(200);
        mHeaderView.startAnimation(headAnim);

        mContentView.layout(mContentInitRect.left, mContentInitRect.top, mContentInitRect.right, mContentInitRect.bottom);
        TranslateAnimation contentAnim = new TranslateAnimation(0, 0, mContentTop - mContentInitRect.top, 0);
        contentAnim.setDuration(200);
        mContentView.startAnimation(contentAnim);
        mIsMoving = false;
    }
}
break;

首先判断当前view是否移动,如果移动了,则返回;有关返回部分的代码,我就不再细了,如果有不理解的同学,可以参考上篇文章《PullScrollView详解(二)——Animation、Layout与下拉回弹》;
这时候的效果基本上就完成了,效果图如下:
这里写图片描述
###四、BUG修复及优化
####1、拦截点击事件
首先,有关事件拦截的问题,大家可以先看这篇文章:《Android-onInterceptTouchEvent()和onTouchEvent()总结》,看完这篇文章以后,大家可以会了解到,有关Touch拦截与分发消费的问题。
假如,我们在MainActivity中,添加了如下代码:

public void showTable() {
        …………
        tableRow.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), "Click item " + n, Toast.LENGTH_SHORT).show();
            }
        });
        …………
    }

即,当用户点击TableLayout的某一个ITEM时,弹出一个TOAST。换句话说,就是在PullScrollView的子控件中消费了点击事件。
那现在我们再运行代码,这时候就出现了问题:
在这里插入图片描述

在录像上可能看不出来,大家仔细看,在headview上点击向下拖动是没有问题的;但当用手指放在contentview上向下拖动的时候,contentview会突然下滑一下,然后再向下拖动。
刚开始,我也没理解这是怎么回事,后来经过打LOG发现,,MotionEvent.ACTION_DOWN里的代码根本没有运行,这是为什么?
想必大家看过上面的《Android-onInterceptTouchEvent()和onTouchEvent()总结》之后,应该就能理解出来,OnTouchEvent()走的是事件的消费阶段,只有它的所有子控件不消费的事件才会传到它这来。也就是说,只有PullScrollView中的所有子控件都不消费的事件,才会传递到PullScrollView的OnTouchEvent()中来处理。而我们在TableLayout中已经为每个ITEM添加了onClick()事件的响应,也就是已经消费了点击事件,所以,PullScrollView肯定就不会再收到点击相关的事件了,因为已经被它的子控件消费掉了。
所以,我们要在事件分发阶段拦截点击事件,以保证肯定被运行。要在分发阶段拦截,就需要重写onInterceptTouchEvent()函数,在其中做处理:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        //保存原始位置
        mTouchPoint.set((int) event.getX(), (int) event.getY());
        mHeadInitRect.set(mHeaderView.getLeft(), mHeaderView.getTop(), mHeaderView.getRight(), mHeaderView.getBottom());
        mContentInitRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom());
        mIsMoving = false;
    }
    return super.onInterceptTouchEvent(event);
}

####2、拦截MOVE事件
同理,我们不能祈祷PullScrollView的所有子控件都不处理MotionEvent.ACTION_MOVE事件,我们必须拦截MOVE事件,以保证我们的代码一定能够运行,同样,我们要重写onInterceptTouchEvent()函数,而且在要保证我们在移动的时候,不能把ACTION_MOVE事件传递给子控件;也就是我们在移动的时候禁止所有子控件的对MOVE事件的捕捉,所以要返回true,所以完整的onInterceptTouchEvent()代码如下:

public boolean onInterceptTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        //保存原始位置
        mTouchPoint.set((int) event.getX(), (int) event.getY());
        mHeadInitRect.set(mHeaderView.getLeft(), mHeaderView.getTop(), mHeaderView.getRight(), mHeaderView.getBottom());
        mContentInitRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom());
        mIsMoving = false;
    }else if(event.getAction() == MotionEvent.ACTION_MOVE){
        //如果当前的事件是我们要处理的事件时,比如现在的下拉,这时候,我们就不能让子控件来处理这个事件
        //这里就需要把它截获,不传给子控件,更不能让子控件消费这个事件
        //不然子控件的行为就可能与我们的相冲突
        int deltaY = (int) event.getY() - mTouchPoint.y;
        deltaY = deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY;
        if (deltaY > 0 && deltaY >= getScrollY()) {
            onTouchEvent(event);
            return true;
        }
    }
    return super.onInterceptTouchEvent(event);
}

通过返回true,来禁止事件继续传递。
####3、拖动时禁止ScollView本身的滚动行为
我们要做到,我们移动布局时不受任何的干扰,在上面我们在通过拦截ACTION_MOVE事件来禁止PullScrollView所有子控件的移动;由于我们的PullScrollView派生自ScrollView,除了禁止子控件的移动以外,还要在移动时禁止ScrollView自身的滚动行为。
首先,我们定义一个变量来标识当前是否要禁止ScrollView自身的默认行为:

boolean mEnableMoving = false;

然后当我们移动布局时设置为True,其它时候都设置为FALSE:

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE: {
            …………
                if (mContentTop <= mHeaderCurBottom) {
                    mHeaderView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, mHeaderCurBottom);
                    mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, mContentBottom);
                    mEnableMoving = true;
                }
            }
        }
        break;
        case MotionEvent.ACTION_UP: {
	        mEnable = false;
        }
    …………
    }
    return mEnableMoving || super.onTouchEvent(event);
}

最后在return时:

return mEnableMoving || super.onTouchEvent(event);

这句就厉害了,由于mEnableMoving和super.onTouchEvent(event);用的是或运算符,所以当mEnableMoving为true时,就不会执行super.onTouchEvent(event),也就不会执行ScrollView的默认操作;相反,当mEnableMoving为false时,就会执行super.onTouchEvent(event),就会执行ScrollView默认操作。
到这里,基本所有的问题都解决了。下面我们就再优化一下操作。
####4、只允许布局在初始状态时,才允许用户滚动
由于我们在用户点击时获取当前布局的位置的,但当用户先将布局向上滚,然后再点击向下拉。这时我们获取到的初始值就会出错。所以反弹就肯定会出问题。所以我们要想办法,只让在布局在初始状态时,才允许用户拖拽。效果如下:
在这里插入图片描述

可以看到,当向上滚动后再向下拉是拉不动的。只有在初始位置时才能拖动。
在ScrollView中,正好有一个值来判断当前的ScrollView是不是在初始化状态,即getScrollY()是不是等于0,当等于0时,肯定是初始化状态。
所以,首先,定义一个变量来标识当前布局是不是初始化状态:

boolean mIsLayout = false;

在点击的时候,判断当前是不是初始状态:

public boolean onInterceptTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        …………
        //如果当前不是从初始化位置开始滚动的话,就不让用户拖拽
        if (getScrollY() == 0){
            mIsLayout = true;
        }
    } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
        …………
    }
    return super.onInterceptTouchEvent(event);
}

在ACTION_MOVE的时候,判断当前是否能够移动:

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE: {
            int deltaY = (int) event.getY() - mTouchPoint.y;
            deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY);
            if (deltaY > 0 && deltaY >= getScrollY() && mIsLayout) {
               …………
                }
            }
        }
        break;
        …………
}

####5、使用declare-styleable来自定义属性
在代码中,我们的拖动的最大高度,直接是通过headview.getHeight()来获取的:

deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY);

在第一篇中,我们讲述了如何通过declare-styleable来自定义控件属性,这里我们就通过自定义的属性来预定义PullScrollView的这项参数,当然,大家也可以发挥想象,把其它的可变参数,也可以由用户通过参数来自定义。
首先,在values文件夹下新建一个attrs.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PullScrollView">
        <attr name="header" format="reference" />
        <attr name="headerHeight" format="dimension" />
        <attr name="headerTopHeight" format="dimension"/>
        <attr name="headerVisibleHeight" format="dimension" />
    </declare-styleable>
</resources>

我这里除了headerHeight(头部高度)变量,还额外申请了几个,以便大家进一步认识declare-styleable的用法,虽然这里也用不到……
然后,在activity_main.xml中使用:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/background_img"
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:layout_marginTop="-100dp"
        android:scaleType="fitXY"
        android:src="@drawable/pic3" />

    <com.harvic.pullscrollviewrudiment.PullScrollView
        android:id="@+id/pullscrollview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:headerHeight="400dp"
        app:headerTopHeight = "100dp"
        app:headerVisibleHeight="150dp">
            <TableLayout
                android:id="@+id/table_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="150dp"/>
    </com.harvic.pullscrollviewrudiment.PullScrollView>
</FrameLayout>

在XML中使用declare-styleable自定义的属性,前面我们讲过,首先要在根布局添加:

xmlns:app="http://schemas.android.com/apk/res-auto"

然后在使用时,直接使用app:XXXX即可;具体可以参考本系列第一篇。
然后在PullScrollView中得到定义的值并使用:
先是获取定义的值:

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

public PullScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context,attrs);
}

private void init(Context context, AttributeSet attrs) {
    // set scroll mode
    setOverScrollMode(OVER_SCROLL_NEVER);

    if (null != attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PullScrollView);

        if (ta != null) {
            mHeaderHeight = (int) ta.getDimension(R.styleable.PullScrollView_headerHeight, -1);
            ta.recycle();

        }
    }
}

然后在用到mHeaderView.getHeight()的地方改成mHeaderHeight;
即把下面的:

deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY);

改成:

deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY);

好了,到这里基本就全部完成了,源码在文章底部给出

如果本文有帮到你,记得加关注哦
####源码下载地址:http://download.csdn.net/detail/harvic880925/8862541
####请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/46728247 谢谢


如果你喜欢我的文章,你可能更喜欢我的公众号
在这里插入图片描述


参考文章:
1、PullScrollView
2、Android仿IOS回弹效果 ScrollView回弹 总结
3、Android ScrollView回弹效果(二)
4、高仿QQ的上下回弹效果之自定义的ScrollView
5、自定义上拉下拉反弹ScrollView
6、android坐标
7、Android-onInterceptTouchEvent()和onTouchEvent()总结
8、scrollView的fillviewport
9、Android 自定义UI View - 04 圆形图片控件之自定义属性

  • 11
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值