前沿分析:
我为什么要想实现一个这样的回弹呢?因为android都没有支持回弹效果,只有个oversroll的回弹效果,其他的时候都是edgeeffect效果,当我们在哪个地方需要这样的回弹效果我们就直接把我们的控件往这个SrollVIew里面一扔就可以了。其他的都不用管。
主要用到的类讲解:
Scroller,主要来辅助我们记录动画和滑动的类,VelocityTracker用来计算滑动阀值就是快速滑动的辅助类,用到的辅助类就这两个,其他的就是测量和布局还有事件的编写了。
效果图
里面的按钮是我真正的圆角button来了里面的按钮,可以看看,效果还是不错的,这里只是给大家提供了这样的一个思路大家以后下去可以自己改进。
实例代码讲解
首先开始我们还是先来理清一下我们的思路,定义好我们的类,先上我们类的定义吧。
class QSrollView extends ViewGroup { public final static String TAG = QSrollView.class.getSimpleName(); public final static int TOUCH_STATE_SROLLING = 1; // 当前在滑动状态 public final static int TOUCH_STATE_FLING = 2; // 当前fling状态 public final static int TOUCH_STATE_DEFALUT = 0; // 默认 private int mTouchState = TOUCH_STATE_DEFALUT; private int mTouchSlop = 0; // 当前滑动阀值 private int mLastMontionY; // 记录上次y的位置 Scroller mScroller; // 滑动辅助类 private int mTotalLength = 0; // 整个控件的长度 private int mMaxmumVelocity = 0; // Velocity的阀值 private VelocityTracker mVelocityTracker; // Velocity int mPointID = 0; // pointID public QSrollView(Context context) { super(context); init(); } private void init() { mScroller = new Scroller(getContext()); mTouchSlop = ViewConfiguration.getTouchSlop(); mMaxmumVelocity = ViewConfiguration.getMaximumFlingVelocity(); }
显示定义我们的类和初始化我们的变量,这些都很简单,然后我们来写一下它的onMeasure方法
/** * 重写onMeasure方法计算 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int size = getChildCount(); final int parentWidthSize = MeasureSpec.getSize(widthMeasureSpec); final int paretnHeightSize = MeasureSpec.getSize(heightMeasureSpec); for (int i = 0; i < size; ++i) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams childLp = child.getLayoutParams(); final boolean childWidthWC = childLp.width == LayoutParams.WRAP_CONTENT; final boolean childHeightWC = childLp.height == LayoutParams.WRAP_CONTENT; int childWidthMeasureSpec; int childHeightMeasureSpec; if (child.getLayoutParams() instanceof MarginLayoutParams) { MarginLayoutParams childMarginLp = (MarginLayoutParams) childLp; childWidthMeasureSpec = childWidthWC ? MeasureSpec .makeMeasureSpec(parentWidthSize, MeasureSpec.UNSPECIFIED) : getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight() + childMarginLp.leftMargin + childMarginLp.rightMargin, childLp.width); childHeightMeasureSpec = childHeightWC ? MeasureSpec .makeMeasureSpec(paretnHeightSize, MeasureSpec.UNSPECIFIED) : getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom() + childMarginLp.topMargin + childMarginLp.bottomMargin, childMarginLp.height); } else { childWidthMeasureSpec = childWidthWC ? MeasureSpec .makeMeasureSpec(parentWidthSize, MeasureSpec.UNSPECIFIED) : getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), childLp.width); childHeightMeasureSpec = childHeightWC ? MeasureSpec .makeMeasureSpec(paretnHeightSize, MeasureSpec.UNSPECIFIED) : getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), childLp.height); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
计算它每一个孩子的高度,这里这里我直接用的系统的MarginLayoutParams,可以实现我们控件的margin效果,还有就是我们写的这个Scrollview可以add多次child不像系统提供的ScrollVIew一样只能add一个孩子,最后记得一定要调用一下这个函数super.onMeasure(widthMeasureSpec, heightMeasureSpec);其实在底层是调用的setMeasuredDimension这个函数,它的作用是告诉控件自己测量完了大小是多少,然后接下来就是我们的OnLayout方法了。
/*** * 重写layout方法 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childStartPostion = 0; mTotalLength = 0; final int count = getChildCount(); if (count == 0) { return; } childStartPostion = getPaddingTop(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child != null && child.getVisibility() != View.GONE) { LayoutParams lp = child.getLayoutParams(); final int childHeight = child.getMeasuredHeight(); int leftMargin = 0; int rightMargin = 0; int topMargin = 0; int bottomMargin = 0; if (lp instanceof MarginLayoutParams) { MarginLayoutParams mlp = (MarginLayoutParams) lp; leftMargin = mlp.leftMargin; rightMargin = mlp.rightMargin; topMargin = mlp.topMargin; bottomMargin = mlp.bottomMargin; } childStartPostion += topMargin; int startX = (getWidth() - leftMargin - rightMargin - child .getMeasuredWidth()) / 2 + leftMargin; child.layout(startX, childStartPostion, startX + child.getMeasuredWidth(), childStartPostion + childHeight); childStartPostion += (childHeight + bottomMargin); } } childStartPostion += getPaddingBottom(); mTotalLength = childStartPostion; }
onlayout方法很简单但是要注意一点的是,我们是从上到下的的放置我们的控件的,就是我们的垂直布局。然后记录我们所有的控件的总共高度。如果这两部没有问题,那么我们的控件就应该可以跑起来了,可以当一个LiearLayout的垂直布局用了,但是没有居中这些属性。
过了就是我们的事件拦截的写法了。
/** * 事件拦截 */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); // 表示已经开始滑动了,不需要走该Action_MOVE方法了(第一次时可能调用)。 if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_DEFALUT)) { return true; } int y = (int) ev.getY(); switch (action) { case MotionEvent.ACTION_MOVE: final int xDiff = (int) Math.abs(mLastMontionY - y); // 超过了最小滑动距离 if (xDiff > mTouchSlop) { mTouchState = TOUCH_STATE_SROLLING; } break; case MotionEvent.ACTION_POINTER_DOWN: mPointID = ev.getPointerId(ev.getActionIndex()); // 记录当前pointID break; case MotionEvent.ACTION_DOWN: mLastMontionY = y; Log.e(TAG, mScroller.isFinished() + ""); if (!mScroller.isFinished()) // 当动画还没有结束的时候强制结束 { mScroller.abortAnimation(); mScroller.forceFinished(true); } mTouchState = TOUCH_STATE_DEFALUT; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mTouchState = TOUCH_STATE_DEFALUT; break; } Log.e(TAG, mTouchState + "====" + TOUCH_STATE_DEFALUT); return mTouchState != TOUCH_STATE_DEFALUT; }
事件拦截也比较简单,就是在滑动超过我们的阀值的时候改变当前滑动的状态并拦截,不让它忘子控件里面分发事件了。需要注意的是在ACTION_POINTER_DOWN这个事件的时候我们需要记录它的PointId因为这样在后面的一个手指不放开,另外一个手指又上去滑动的时候才有效果。重点来了我们的onTouchEvent方法。
@Override public boolean onTouchEvent(MotionEvent event) { int touchIndex = event.getActionIndex(); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPointID = event.getPointerId(0); mLastMontionY = (int) event.getY();// 记录按下的点 break; case MotionEvent.ACTION_POINTER_DOWN: // 添加多点触控的处理 mPointID = event.getPointerId(touchIndex); mLastMontionY = (int) (event.getY(touchIndex) + 0.5f); // 记录按下的点 break; case MotionEvent.ACTION_MOVE: touchIndex = event.findPointerIndex(mPointID); if (touchIndex < 0) // 当前index小于0就返false继续接受下一次事件 return false; int detaY = (int) (mLastMontionY - event.getY(touchIndex)); // 计算滑动的距离 scrollBy(0, detaY); // 调用滑动函数 mLastMontionY = (int) event.getY(touchIndex); // 记录上一次按下的点 break; case MotionEvent.ACTION_UP: Log.d("edsheng", "Action UP"); mVelocityTracker.computeCurrentVelocity(1000); if (Math.abs(mVelocityTracker.getYVelocity()) > mMaxmumVelocity&&!checkIsBroad()) { mScroller.fling(getScrollX(), getScrollY(), 0,-(int) mVelocityTracker.getYVelocity(), 0, 0, 0, mTotalLength - getHeight()); } else { actionUP(); // 回弹效果 } mTouchState = TOUCH_STATE_DEFALUT; break; case MotionEvent.ACTION_POINTER_UP: // 添加多点触控的支持 if (event.getPointerId(touchIndex) == mPointID) { final int newIndex = touchIndex == 0 ? 1 : 0; mPointID = event.getPointerId(newIndex); mLastMontionY = (int) (event.getY(newIndex) + 0.5f); } break; case MotionEvent.ACTION_CANCEL: mTouchState = TOUCH_STATE_DEFALUT; break; default: break; } // super.onTouchEvent(event); return true; }
可以看到在touch方法里面主要是在ACTION_MOVE的时候调用我们的 scrollBy(0, detaY); 这个函数去滚动我们的视图,然后在ACTION_UP事件里面处理回弹还是fling效果。很重要的一点说好的阻力效果呢?我们在滑动的时候是不是调用了ScrollBy这个方法呢?每次都去滑动,然后我们就看看到底咋个来实现阻力效果的?
@Override public void scrollBy(int x, int y) { // 判断当前视图是否超过了顶部或者顶部就让它滑动的距离为1/3这样就有越拉越拉不动的效果 if (getScrollY() < 0 || getScrollY() + getHeight() > mTotalLength) { super.scrollBy(x, y / 3); } else { super.scrollBy(x, y); } }
看到了吗?当越界或者在底部的时候就让它滑动的时候为原来的1/3这样就有越来越滑不动的效果了,简单有效。然后就是我们的回弹了,看看我们的回弹吧。
/** * 回弹函数 */ private void actionUP() { if (getScrollY() < 0 || getHeight() > mTotalLength) // 顶部回弹 { Log.d("edsheng", "顶部回弹!!!!"); mScroller.startScroll(0, getScrollY(), 0, -getScrollY()); // 开启回弹效果 invalidate(); } else if (getScrollY() + getHeight() > mTotalLength) // 底部回弹 { // 开启底部回弹 mScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + getHeight() - mTotalLength)); invalidate(); } }
这里就用到了我们Scroller去帮助我们计算动画,重要的一点是在调用了startScroll的时候一定要记得invalidate一下。还有就是当计算到动画的时候我们要重写一些这个computeScroll这个方法是系统在重绘的时候会自动调用这个方法。
@Override public void computeScroll() { if (mScroller.computeScrollOffset()) // 计算当前位置 { // 滚动 scrollTo(0, mScroller.getCurrY()); postInvalidate(); } }
最后贴上全部的类代码。
package com.example.scolview; import android.content.Context; 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.ListView; import android.widget.ScrollView; import android.widget.Scroller; import java.io.InputStream; /* * @FileName:QSrollView.java * @Version:V1.0 * @Date: 2015-2-1 Create * @author: edsheng * */ class QSrollView extends ViewGroup { public final static String TAG = QSrollView.class.getSimpleName(); public final static int TOUCH_STATE_SROLLING = 1; // 当前在滑动状态 public final static int TOUCH_STATE_FLING = 2; // 当前fling状态 public final static int TOUCH_STATE_DEFALUT = 0; // 默认 private int mTouchState = TOUCH_STATE_DEFALUT; private int mTouchSlop = 0; // 当前滑动阀值 private int mLastMontionY; // 记录上次y的位置 Scroller mScroller; // 滑动辅助类 private int mTotalLength = 0; // 整个控件的长度 private int mMaxmumVelocity = 0; // Velocity的阀值 private VelocityTracker mVelocityTracker; // Velocity int mPointID = 0; // pointID public QSrollView(Context context) { super(context); init(); } private void init() { mScroller = new Scroller(getContext()); mTouchSlop = ViewConfiguration.getTouchSlop(); mMaxmumVelocity = ViewConfiguration.getMaximumFlingVelocity(); } @Override public void scrollBy(int x, int y) { // 判断当前视图是否超过了顶部或者顶部就让它滑动的距离为1/3这样就有越拉越拉不动的效果 if (getScrollY() < 0 || getScrollY() + getHeight() > mTotalLength) { super.scrollBy(x, y / 3); } else { super.scrollBy(x, y); } } /** * 事件拦截 */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); // 表示已经开始滑动了,不需要走该Action_MOVE方法了(第一次时可能调用)。 if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_DEFALUT)) { return true; } int y = (int) ev.getY(); switch (action) { case MotionEvent.ACTION_MOVE: final int xDiff = (int) Math.abs(mLastMontionY - y); // 超过了最小滑动距离 if (xDiff > mTouchSlop) { mTouchState = TOUCH_STATE_SROLLING; } break; case MotionEvent.ACTION_POINTER_DOWN: mPointID = ev.getPointerId(ev.getActionIndex()); // 记录当前pointID break; case MotionEvent.ACTION_DOWN: mLastMontionY = y; Log.e(TAG, mScroller.isFinished() + ""); if (!mScroller.isFinished()) // 当动画还没有结束的时候强制结束 { mScroller.abortAnimation(); mScroller.forceFinished(true); } mTouchState = TOUCH_STATE_DEFALUT; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mTouchState = TOUCH_STATE_DEFALUT; break; } Log.e(TAG, mTouchState + "====" + TOUCH_STATE_DEFALUT); return mTouchState != TOUCH_STATE_DEFALUT; } @Override public boolean onTouchEvent(MotionEvent event) { int touchIndex = event.getActionIndex(); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPointID = event.getPointerId(0); mLastMontionY = (int) event.getY();// 记录按下的点 break; case MotionEvent.ACTION_POINTER_DOWN: // 添加多点触控的处理 mPointID = event.getPointerId(touchIndex); mLastMontionY = (int) (event.getY(touchIndex) + 0.5f); // 记录按下的点 break; case MotionEvent.ACTION_MOVE: touchIndex = event.findPointerIndex(mPointID); if (touchIndex < 0) // 当前index小于0就返false继续接受下一次事件 return false; int detaY = (int) (mLastMontionY - event.getY(touchIndex)); // 计算滑动的距离 scrollBy(0, detaY); // 调用滑动函数 mLastMontionY = (int) event.getY(touchIndex); // 记录上一次按下的点 break; case MotionEvent.ACTION_UP: Log.d("edsheng", "Action UP"); mVelocityTracker.computeCurrentVelocity(1000); if (Math.abs(mVelocityTracker.getYVelocity()) > mMaxmumVelocity&&!checkIsBroad()) { mScroller.fling(getScrollX(), getScrollY(), 0,-(int) mVelocityTracker.getYVelocity(), 0, 0, 0, mTotalLength - getHeight()); } else { actionUP(); // 回弹效果 } mTouchState = TOUCH_STATE_DEFALUT; break; case MotionEvent.ACTION_POINTER_UP: // 添加多点触控的支持 if (event.getPointerId(touchIndex) == mPointID) { final int newIndex = touchIndex == 0 ? 1 : 0; mPointID = event.getPointerId(newIndex); mLastMontionY = (int) (event.getY(newIndex) + 0.5f); } break; case MotionEvent.ACTION_CANCEL: mTouchState = TOUCH_STATE_DEFALUT; break; default: break; } // super.onTouchEvent(event); return true; } /** * 回弹函数 */ private void actionUP() { if (getScrollY() < 0 || getHeight() > mTotalLength) // 顶部回弹 { Log.d("edsheng", "顶部回弹!!!!"); mScroller.startScroll(0, getScrollY(), 0, -getScrollY()); // 开启回弹效果 invalidate(); } else if (getScrollY() + getHeight() > mTotalLength) // 底部回弹 { // 开启底部回弹 mScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + getHeight() - mTotalLength)); invalidate(); } } /*** * 检测当前是否可回弹 * * @return */ boolean checkIsBroad() { if (getScrollY() < 0 || getScrollY() + getHeight() > mTotalLength) // 顶部回弹) // //顶部回弹 return true; else return false; } /** * 重写onMeasure方法计算 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int size = getChildCount(); final int parentWidthSize = MeasureSpec.getSize(widthMeasureSpec); final int paretnHeightSize = MeasureSpec.getSize(heightMeasureSpec); for (int i = 0; i < size; ++i) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams childLp = child.getLayoutParams(); final boolean childWidthWC = childLp.width == LayoutParams.WRAP_CONTENT; final boolean childHeightWC = childLp.height == LayoutParams.WRAP_CONTENT; int childWidthMeasureSpec; int childHeightMeasureSpec; if (child.getLayoutParams() instanceof MarginLayoutParams) { MarginLayoutParams childMarginLp = (MarginLayoutParams) childLp; childWidthMeasureSpec = childWidthWC ? MeasureSpec .makeMeasureSpec(parentWidthSize, MeasureSpec.UNSPECIFIED) : getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight() + childMarginLp.leftMargin + childMarginLp.rightMargin, childLp.width); childHeightMeasureSpec = childHeightWC ? MeasureSpec .makeMeasureSpec(paretnHeightSize, MeasureSpec.UNSPECIFIED) : getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom() + childMarginLp.topMargin + childMarginLp.bottomMargin, childMarginLp.height); } else { childWidthMeasureSpec = childWidthWC ? MeasureSpec .makeMeasureSpec(parentWidthSize, MeasureSpec.UNSPECIFIED) : getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), childLp.width); childHeightMeasureSpec = childHeightWC ? MeasureSpec .makeMeasureSpec(paretnHeightSize, MeasureSpec.UNSPECIFIED) : getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), childLp.height); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } /*** * 重写layout方法 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childStartPostion = 0; mTotalLength = 0; final int count = getChildCount(); if (count == 0) { return; } childStartPostion = getPaddingTop(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child != null && child.getVisibility() != View.GONE) { LayoutParams lp = child.getLayoutParams(); final int childHeight = child.getMeasuredHeight(); int leftMargin = 0; int rightMargin = 0; int topMargin = 0; int bottomMargin = 0; if (lp instanceof MarginLayoutParams) { MarginLayoutParams mlp = (MarginLayoutParams) lp; leftMargin = mlp.leftMargin; rightMargin = mlp.rightMargin; topMargin = mlp.topMargin; bottomMargin = mlp.bottomMargin; } childStartPostion += topMargin; int startX = (getWidth() - leftMargin - rightMargin - child .getMeasuredWidth()) / 2 + leftMargin; child.layout(startX, childStartPostion, startX + child.getMeasuredWidth(), childStartPostion + childHeight); childStartPostion += (childHeight + bottomMargin); } } childStartPostion += getPaddingBottom(); mTotalLength = childStartPostion; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) // 计算当前位置 { // 滚动 scrollTo(0, mScroller.getCurrY()); postInvalidate(); } } }
然后我们在我们的Activity里面这样使用就可以了。
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); QSrollView qSrollView = new QSrollView(this); // qbSrollView.setBackgroundColor(Color.RED); qSrollView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); LinearLayout.LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); layoutParams.bottomMargin = 20; LayoutParams commomlayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); commomlayoutParams.bottomMargin = 20; StyleButton button = new StyleButton(this); button.setText("按钮"); qSrollView.addView(button,commomlayoutParams); StyleButton button2 = new StyleButton(this); button2.setText("按钮"); button2.setRadius(16); qSrollView.addView(button2,commomlayoutParams); StyleButton button3 = new StyleButton(this); button3.setText("按钮"); button3.setRadius(32); button3.setTextNormalPressedcolor(Color.CYAN, Color.WHITE); button3.setBgNormalPressedcolor(Color.BLUE, Color.CYAN); qSrollView.addView(button3,commomlayoutParams); // for(int i= 0 ; i < 100; i ++) { StyleButton button4 = new StyleButton(this); // button4.setText("按钮"+i); button4.setRadius(80); button4.setBgNormalPressedcolor(Color.GRAY, Color.CYAN); button4.setTextNormalPressedcolor(Color.RED, Color.WHITE); qSrollView.addView(button4,commomlayoutParams); } StyleButton button5 = new StyleButton(this); button5 = new StyleButton(this); button5.setText("按钮"); button5.setRadius(50); button5.setTextNormalPressedcolor(Color.BLACK, Color.BLUE); button5.setBgNormalPressedcolor(Color.WHITE, Color.CYAN); qSrollView.addView(button5,commomlayoutParams); StyleButton button6 = new StyleButton(this); button6.setText("按钮"); button6.setRadius(50); button6.setTextNormalPressedcolor(Color.BLACK, Color.CYAN); button6.setBgNormalPressedcolor(Color.CYAN, Color.BLUE); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); params.leftMargin = 80; params.rightMargin = 80; qSrollView.addView(button6,commomlayoutParams); StyleButton button7 = new StyleButton(this); button7.setText("按钮"); button7.setRadius(80); qSrollView.addView(button7,commomlayoutParams); setContentView(qSrollView); }; }
里面用到的按钮还是我上一篇里面的button可以看看真正的圆角Button好了今天的教程就到这里了,写得不好还请原谅。有什么不足我们也一起进步。demo下载地址