本来上周六晚上出去散步的时候就随便想了下,当时的想法是ViewGroup要实现内部控件的滚动,1. 最终效果肯定就是子控件的重绘对吧? 2. 重绘肯定就涉及到onLayout重新定位的处理对吧? 重新定位+重新绘制理论上就是实现滚动的原理了吧。
基于上述猜测,小白以为我们只要在onLayout中重新刷新控件的位置不就可以实现滚动了么?没错,小白实践了,可以滴?--需要了解如下知识:
Invalidate:
To farce a view to draw,call invalidate().——摘自View类源码
从上面这句话看出,invalidate方法会执行draw过程,重绘View树。
当View的appearance发生改变,比如状态改变(enable,focus),背景改变,隐显改变等,这些都属于appearance范畴,都会引起invalidate操作。
所以当我们改变了View的appearance,需要更新界面显示,就可以直接调用invalidate方法。
View(非容器类)调用invalidate方法只会重绘自身,ViewGroup调用则会重绘整个View树。
RequestLayout:
To initiate a layout, call requestLayout(). This method is typically called by a view on itself when it believes that it can no longer fit within its current bounds.——摘自View源码
从上面这句话看出,当View的边界,也可以理解为View的宽高,发生了变化,不再适合现在的区域,可以调用requestLayout方法重新对View布局。
View执行requestLayout方法,会向上递归到顶级父View中,再执行这个顶级父View的requestLayout,所以其他View的onMeasure,onLayout也可能会被调用。
requestLayout()方法就是我们请求重新测量和定位的方式。
**1. **比如我们实现一个简单的效果有问题的上下滑动的ViewGroup
1.1 首先获取上下滑动事件处理
1.2 获得上下滑动分别的距离
1.3 滑动的距离分别附加到onlayout的leftTop上,也就是原来控件的摆放的初始位置全部加上这个偏移量(该偏移量如果是向上滑动为负数, 向下自然就是整数;该便宜是累计的,在onTouchEvent中做简单计算哟)
So, 看简单的偏移量的计算....应该不难吧...
private float x1, x2;
private float y1, y2;
private float swing = 0;
private float totalswings = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
y1 = event.getY();
break;
case MotionEvent.ACTION_MOVE:
x2 = event.getX();
y2 = event.getY();
///< 5作为阀值就可以了,可以根据效果调整
if(y1 - y2 > 5) { ///< 上
swing = y1 - y2;
totalswings += swing;
requestLayout();
Log.e("test", "上滑动");
} else if(y2 - y1 > 5) { ///< 下
swing = -(y2 - y1);
totalswings += swing;
requestLayout();
Log.e("test", "下滑动");
} else if(x1 - x2 > 50) { ///< 左
} else if(x2 - x1 > 50) { ///< 右
}
x1 = x2;
y1 = y2;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
Then, 拿到偏移累计后totalswings,就可以在onLayout中进行偏移添加:
效果就是这么粗糙啦(上下错了这些不要在意,关键是我们知道这样可以做,具体效果估计要搞一些好的算法处理才行)..
2. 或许我们可以像上面那样做,但是感觉有点麻烦的样子...再查看一些个资料,发现其实大部分码友的做法是调用系统的scroll相关的方法 - 看官方
scrollBy
added in API level 1
public void scrollBy (int x,
int y)
Move the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.
Parameters
x int: the amount of pixels to scroll by horizontally
y int: the amount of pixels to scroll by vertically
scrollTo
added in API level 1
public void scrollTo (int x,
int y)
Set the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.
Parameters
x int: the x position to scroll to
y int: the y position to scroll to
scrollBy、scrollTo,两个来自View的方法。ViewGroup继承自View,自然也可以直接调用该方法。
scrollBy - 滚动的距离(用这个来实现滚动的效果即可,每次onTouch我们都计算了滑动偏移量)
scrollTo - 滚动到哪里
So, 我们改造下onTouchEvent方法:
private float x1, x2;
private float y1, y2;
private float swing = 0;
private float totalswings = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
y1 = event.getY();
break;
case MotionEvent.ACTION_MOVE:
x2 = event.getX();
y2 = event.getY();
///< 5作为阀值就可以了,可以根据效果调整
if(y1 - y2 > 5) { ///< 上
swing = y1 - y2;
///< 采用刷新onLahyout实现滚动
//totalswings += swing;
//requestLayout();
///< 采用系统View类的滚动方法
scrollBy(0, (int) swing);
Log.e("test", "上滑动");
} else if(y2 - y1 > 5) { ///< 下
swing = -(y2 - y1);
///< 采用刷新onLahyout实现滚动
//totalswings += swing;
//requestLayout();
///< 采用系统View类的滚动方法
scrollBy(0, (int) swing);
Log.e("test", "下滑动");
} else if(x1 - x2 > 50) { ///< 左
} else if(x2 - x1 > 50) { ///< 右
}
x1 = x2;
y1 = y2;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
效果一般般啦:
2.1 稍微看下scrollBy源码吧....
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
最后调用的其实是scrollTo
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
关注下下面这两个变量哈....
再往每个具体的函数跟,其实还是复杂。我们换个思路,既然最终是靠定位和重绘实现的滚动,那么我们去看看TextView的onDraw:
小白框起来的些个变量,有没有想到一些联系(小白大体跟了下:当调用scrollBy时,会更新对应的mScrollX/Y,然后请求相关控件的重定位,重绘,进而达到滚动效果)。。。其实子控件再考虑绘制的时候已经考虑到了父控件相关的滚动设计。 当然要彻底搞明白整个流程,肯定不是一天两天的事情(对小白来说).
不过对小白来讲,能学到了解到上面些的知识,还是蛮不错的。原理还是有点领悟的....
**3. 当然如果你搜索网上相关的资料,会发现有些码友用的是Scroller - 所谓的弹性辅助滑动类 **Scroller | Android Developers
Scroller就是一个滑动帮助类。它并不可以使View真正的滑动,
而是配合scrollTo/ScrollBy让view产生缓慢的滑动,产生动画的效果,其实和属性动画是同一个原理。
在我看来,Scroller跟属性动画的平移的效果是一样的。
我们可以试试(具体的用法和效果可以专门学习下,然后用到我们的自定义ViewGroup中).
使用方式是这样的:
To track the changing positions of the x/y coordinates,
use computeScrollOffset().
The method returns a boolean to indicate whether the scroller is finished.
If it isn't, it means that a fling or programmatic pan operation is still in progress.
You can use this method to find the current offsets of the x and y coordinates, for example:
if (mScroller.computeScrollOffset()) {
// Get current x and y positions
int currX = mScroller.getCurrX();
int currY = mScroller.getCurrY();
...
}
3.1 调用mScroller.startScroll(0, 0, 100, 0);
// Invalidate to request a redraw
invalidate();
3.2. 根据上面解释,此时就会触发computeScroll()方法的回调:
然后就可以进行类似如下的处理scrollTo、scrollBy看你需要:
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
比如我们这样的处理,当然效果肯定不对,不过先不管了哟!
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
y1 = event.getY();
break;
case MotionEvent.ACTION_MOVE:
x2 = event.getX();
y2 = event.getY();
///< 5作为阀值就可以了,可以根据效果调整
if(y1 - y2 > 5) { ///< 上
swing = y1 - y2;
///< 采用刷新onLahyout实现滚动
//totalswings += swing;
//requestLayout();
///< 采用系统View类的滚动方法
//scrollBy(0, (int) swing);
mScroller.startScroll(0, 0, 0, (int) swing);
invalidate();
Log.e("test", "上滑动");
} else if(y2 - y1 > 5) { ///< 下
swing = -(y2 - y1);
///< 采用刷新onLahyout实现滚动
//totalswings += swing;
//requestLayout();
///< 采用系统View类的滚动方法
//scrollBy(0, (int) swing);
mScroller.startScroll(0, 0, 0, (int) swing);
invalidate();
Log.e("test", "下滑动");
} else if(x1 - x2 > 50) { ///< 左
} else if(x2 - x1 > 50) { ///< 右
}
x1 = x2;
y1 = y2;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
效果:基本滑不动,哈哈~~~先飘过,不管了....
综上我们就大概对这个滚动有了总体的认识、以及对scroll和Scroller的综合理解。作为知识进阶,总得了解更多,虽然远远不够!!
有个疑问需要提下?后面具体研究估计才懂,有懂的可以指教下 - 就是为什么滚动了,但是onLayout并没有进行回调,那怎么实现的这个子控件的重定位,难道仅仅是重绘?
最后看下整体源码吧:CustomViewGroupLastNew.java
package me.heyclock.hl.customcopy;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
/*
*@Description: 自定义ViewGroup + 纵向垂直布局 + 单列 + 粗糙上下滑动
*@Author: hl
*@Time: 2018/10/25 10:18
*/
public class CustomViewGroupLastNew extends ViewGroup {
private Context context;///< 上下文
/**
* 计算子控件的布局位置.
*/
private final Rect mTmpContainerRect = new Rect();
private final Rect mTmpChildRect = new Rect();
/**
* 滚动相关(上下滑动)
*/
private float x1, x2;
private float y1, y2;
private float swing = 0;
private float totalswings = 0;
private Scroller mScroller;
public CustomViewGroupLastNew(Context context) {
this(context, null);
}
public CustomViewGroupLastNew(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomViewGroupLastNew(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, 0);
}
public CustomViewGroupLastNew(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
mScroller = new Scroller(context);
}
/**
* 测量容器的宽高 = 所有子控件的尺寸 + 容器本身的尺寸 -->综合考虑
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
///< 定义最大宽度和高度
int maxWidth = 0;
int maxHeight = 0;
///< 获取子控件的个数
int count = getChildCount();
for (int i = 0; i < count; ++i) {
View view = getChildAt(i);
///< 子控件如果是GONE - 不可见也不占据任何位置则不进行测量
if (view.getVisibility() != GONE) {
///< 获取子控件的属性 - margin、padding
CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();
///< 调用子控件测量的方法getChildMeasureSpec(先不考虑margin、padding)
///< - 内部处理还是比我们自己的麻烦的,后面我们可能要研究和参考
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, layoutParams.leftMargin + layoutParams.rightMargin, layoutParams.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, layoutParams.topMargin + layoutParams.bottomMargin, layoutParams.height);
///< 然后真正测量下子控件 - 到这一步我们就对子控件进行了宽高的设置了咯
view.measure(childWidthMeasureSpec, childHeightMeasureSpec);
///< 然后再次获取测量后的子控件的属性
layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();
///< 然后获取宽度的最大值、高度的累加
maxWidth = Math.max(maxWidth, view.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin);
maxHeight += view.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin;
}
}
///< 然后再与容器本身的最小宽高对比,取其最大值 - 有一种情况就是带背景图片的容器,要考虑图片尺寸
maxWidth = Math.max(maxWidth, getMinimumWidth());
maxHeight = Math.max(maxHeight, getMinimumHeight());
///< 然后根据容器的模式进行对应的宽高设置 - 参考我们之前的自定义View的测试方式
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
///< wrap_content的模式
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(
maxWidth + getPaddingLeft() + getPaddingRight(),
maxHeight + getPaddingTop() + getPaddingBottom());
}
///< 精确尺寸的模式
else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
setMeasuredDimension(wSize, hSize);
}
///< 宽度尺寸不确定,高度确定
else if (wSpecMode == MeasureSpec.UNSPECIFIED) {
setMeasuredDimension(maxWidth + getPaddingLeft() + getPaddingRight(), hSize);
}
///< 宽度确定,高度不确定
else if (hSpecMode == MeasureSpec.UNSPECIFIED) {
setMeasuredDimension(wSize, maxHeight + getPaddingTop() + getPaddingBottom());
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.e("test", "onLayout(" + left + "," + top + "," + right + "," + bottom + ")");
///< 获取范围初始左上角 - 这个决定子控件绘制的位置,我们绘制理论可以从0,0开始,margin容器本身已经考虑过了...所以别和margin混淆了
int leftPos = getPaddingLeft();
int leftTop = getPaddingTop() + (int)totalswings;
///< 获取范围初始右下角 - 如果考虑控件的位置,比如靠右,靠下等可能就要利用右下角范围来进行范围计算了...
///< 后面我们逐步完善控件的时候用会用到这里...
//int rightPos = right - left - getPaddingRight();
//int rightBottom = bottom - top - getPaddingBottom();
///< 由于我们是垂直布局,并且一律左上角开始绘制的情况下,我们只需要计算出leftPos, leftTop就可以了
int count = getChildCount();
for (int i = 0; i < count; ++i){
View childView = getChildAt(i);
///< 控件占位的情况下进行计算
if (childView.getVisibility() != GONE){
///< 获取子控件的属性 - margin、padding
CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) childView.getLayoutParams();
int childW = childView.getMeasuredWidth();
int childH = childView.getMeasuredHeight();
///< 先不管控件的margin哈!
int cleft = leftPos + layoutParams.leftMargin;
int cright = cleft + childW;
int ctop = leftTop + layoutParams.topMargin;
int cbottom = ctop + childH;
///< 下一个控件的左上角需要向y轴移动上一个控件的高度 - 不然都重叠了!
leftTop += childH + layoutParams.topMargin + layoutParams.bottomMargin;
///< 需要一个范围,然后进行摆放
childView.layout(cleft, ctop, cright, cbottom);
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int count = getChildCount();
for (int i = 0; i < count; ++i) {
///< 获取子控件的宽高
View view = getChildAt(i);
Log.e("test", "getPaddingLeft()=" + view.getPaddingLeft());
Log.e("test", "getPaddingRight()=" + view.getPaddingRight());
Log.e("test", "getPaddingTop()=" + view.getPaddingTop());
Log.e("test", "getPaddingBottom()=" + view.getPaddingBottom());
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
y1 = event.getY();
break;
case MotionEvent.ACTION_MOVE:
x2 = event.getX();
y2 = event.getY();
///< 5作为阀值就可以了,可以根据效果调整
if(y1 - y2 > 5) { ///< 上
swing = y1 - y2;
///< 采用刷新onLahyout实现滚动
//totalswings += swing;
//requestLayout();
///< 采用系统View类的滚动方法
scrollBy(0, (int) swing);
///< 采用Scroller滚动辅助类配合computeScroll实现上下滚动
//mScroller.startScroll(0, 0, 0, (int) swing);
invalidate();
Log.e("test", "上滑动");
} else if(y2 - y1 > 5) { ///< 下
swing = -(y2 - y1);
///< 采用刷新onLahyout实现滚动
//totalswings += swing;
//requestLayout();
///< 采用系统View类的滚动方法
scrollBy(0, (int) swing);
///< 采用Scroller滚动辅助类配合computeScroll实现上下滚动
//mScroller.startScroll(0, 0, 0, (int) swing);
invalidate();
Log.e("test", "下滑动");
} else if(x1 - x2 > 50) { ///< 左
} else if(x2 - x1 > 50) { ///< 右
}
x1 = x2;
y1 = y2;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
@Override
public CustomViewGroupLastNew.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomViewGroupLastNew.LayoutParams(getContext(), attrs);
}
/**
* 这个是布局相关的属性,最终继承的是ViewGroup.LayoutParams,所以上面我们可以直接进行转换
* --目的是获取自定义属性以及一些使用常量的自定义
*/
public static class LayoutParams extends MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// Pull the layout param values from the layout XML during
// inflation. This is not needed if you don't care about
// changing the layout behavior in XML.
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);
///< TODO 一些属性的自定义
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
}
Next,我看下点击事件以及后续的滑动冲突的处理....快了,快把自定义流程简单过一遍了,加油,么么哒!