说明
带有上拉阻尼的ScrollView。
思路
ScrollView下只能有一个子View,因此只需要将ScrollView下嵌套一个LinearLayout,将ScrollView原本的子View移植到线性布局中,并在线性布局中添加一个空白的view。在上拉的过程中,根据移动的位置,修改最后一个view的高度,这样就相对于当原内容给拉上去了。
用sum记录本次总的位移,根据位移是正是负,决定底部空白view是否有高度。只有底部view有高度,就可以将上拉的部分给顶上去。
核心
最关键的地方在于判断ScrollView是否滚动到最底部。代码如下:
private boolean isBottom() {
int i = getChildAt(0).getHeight() - getHeight();
return getScrollY() >= i;
}
代码
public class UploadScrollView extends ScrollView {
private static final String TAG = "CustomScrollView";
private int sum = 0;//本次移动过程中,总的位移(有阻尼运动)
private static int MAX_OVER_Y;//上拉超过该距离后不再移动
private static int LIMIT_Y;//上拉的距离超过该值后会进行回调
private OnUploadedListener l;
private boolean isFirst = true;
private boolean isShouldOver = false;
private int slop;
private TextView bottom;
private float lastY;
public UploadScrollView(Context context) {
this(context, null);
}
public UploadScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
slop = (int) Math.sqrt(slop);
post(new Runnable() {
@Override
public void run() {
MAX_OVER_Y = getHeight() / 3;
LIMIT_Y = getHeight() / 4;
}
});
}
@Override
protected void onFinishInflate() {//加载成功后,将原布局移动了线性布局中,并添加一个空白view
super.onFinishInflate();
View view = getChildAt(0);
removeViewAt(0);
LinearLayout ll = new LinearLayout(getContext());
ll.setOrientation(LinearLayout.VERTICAL);
ScrollView.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
addView(ll, lp);
bottom = new TextView(getContext());
bottom.setBackgroundColor(Color.TRANSPARENT);
bottom.setHeight(0);
LinearLayout.LayoutParams scP = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
ll.addView(view, scP);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
ll.addView(bottom, layoutParams);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = ev.getY();
if (getScrollY() >= getScrollRangeY()) {
isShouldOver = true;
isFirst = true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
private int getScrollRangeY() {//得到ScrollView最大的滚动距离,子控件的高度-ScrollView本身的高度
return getChildAt(0).getHeight() - getHeight();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float curY = ev.getY();
float deltaY = curY - lastY;
if (Math.abs(deltaY) < slop) {//防手抖
return true;
}
if (isFirst && isShouldOver) {
isShouldOver = deltaY < 0;//点击时已到边界,并且往上拉,才能滚出边界
isFirst = false;
}
if (sum < 0)//sum<0,说明总体已经上拉过一段距离,这里重新进行赋值,是为了纠正sum可能出现的偏差
sum = -bottom.getHeight();
float tempY = deltaY * (MAX_OVER_Y + sum) / MAX_OVER_Y;
if (isShouldOver && (sum < 0 || (sum == 0 && tempY < 0))) {//已经上拉过,或者没有上拉但即将上拉
if (tempY < 0) {//上拉。之所以+tempY,是为了阻尼效果
sum += tempY;
if (Math.abs(sum) > MAX_OVER_Y) {
sum = -MAX_OVER_Y;
}
} else {//deltaY > 0,说明是下滑,缩回去的速度应该跟下滑速度一样
sum += deltaY;
}
upload(sum);
if(sum < 0)
scrollTo(0,getScrollRangeY());
// 内部增高,但并未滚动,所以看不见下面的内容.也就导致了上拉无效果。所以需要scrollTo一下
lastY = curY;
return true;
}
sum += deltaY;
lastY = curY;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (sum < 0 && isShouldOver) {
upload(0);
if (Math.abs(sum) > LIMIT_Y && l != null) {//回调
l.onUpload();
}
}
sum = 0;
isShouldOver = false;
break;
}
return super.onTouchEvent(ev);
}
public void setOnUploadedListener(OnUploadedListener l) {
this.l = l;
}
public interface OnUploadedListener {
void onUpload();
}
private void upload(int location) {
if (location < 0) {//上拉过一段距离,将底层view的高度设置成上拉的高度
bottom.setHeight(-location);
} else {//总体没有上拉,那就将底层view设置成高度为0,即不可见
bottom.setHeight(0);
}
}
}
总结
所有的上拉下拉都可以总结成:
1,判断当前是否应该进行上拉或者下拉——即:是否需要将上拉部分或者下拉部分显露出来。比如scrollView只需要判断scrollY是否到最低或者最上面就行;ListView参考。
2,根据本次总的位移(有一个变量记住本次的位移),判断上拉部分和下拉部分显示部分。而且,在Move事件中,大部分代码也一样。
3,根据总的位移量,判断当前是否应该进行回调。