android 4实例分析,Android View学习笔记(四):Scroller的原理剖析及使用(下)

一、前言

在上一篇文章中,笔者讲述了Scroller的模板代码以及其原理,对它和View的重绘进行了分析,知道了原理后,这篇文章将结合一个Demo来讲述其用法,以加强读者对Scroller的掌握程度。

二、实例

我们先看该实例的效果是怎样的:

c8657df404b2

实例展示

根据图可以看出,当点击按钮后,小球从高处滑落至底部,并且在底部会反弹,我们使用Scroller来实现以上效果。

(1)首先,我们先绘制小球,自定义一个View,在其onDraw()方法完成绘制,以下为ViewA:

public class ViewA extends View {

private final int radius = 50;

public int getRadius() {

return radius;

}

public ViewA(Context context) {

super(context);

}

public ViewA(Context context, AttributeSet attrs) {

super(context, attrs);

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

/**

* 1、先实例化一个Paint对象,该对象充当“画笔”的作用

* 2、设置抗锯齿、画笔颜色等,这里填充为蓝色

* 3、调用canvas的drawCircle方法绘制圆形,

* 第1、2个参数表示坐标,第3个参数表示半径

*/

Paint paint = new Paint();

paint.setAntiAlias(true);

paint.setColor(Color.BLUE);

canvas.drawCircle(50,50,radius,paint);

}

}

(2)接着,由于我们要对这个小球(ViewA)滑动,那么又因为Scroller是对一个View的内容进行滑动的,那么我们自然就会想到可以在这个ViewA外包裹一层LinearLayout,这样对这个LinearLayout进行Scroller滑动,那么里面的ViewA就会跟着滑动了,这里我们新建一个ParentView,继承LinearLayout:

public class ParentView extends LinearLayout {

private Scroller mScroller;

private ViewA viewA;

private int realHeight;

public ParentView(Context context) {

super(context);

}

public ParentView(Context context, AttributeSet attrs) {

super(context, attrs);

//为了实现回弹效果,这里传递一个BounceInterpolator插值器,该插值器专门用于实现回弹效果

mScroller = new Scroller(context, new BounceInterpolator());

}

/**

* 初始化ScrollX、ScrollY,同时获取子View的实例,获取其半径参数

*

* startScroll(int startX, int startY, int dx, int dy, int duration)方法:

* startX、startY表示滑动开始的坐标;dx、dy表示需要位移的距离;duration表示移位的时间

*

* invalidate()方法:在View树重绘的时候会调用computeScrollOffset()方法

*/

public void smoothScrollTo(){

viewA = (ViewA) getChildAt(0);

int ScrollX = getScrollX();

int ScrollY = getScrollY();

realHeight = getHeight()-2*viewA.getRadius();

mScroller.startScroll(ScrollX, 0, 0, -realHeight, 1000);

invalidate();

}

/**

* 先调用computeScrollOffset()方法,计算出新的CurrX和CurrY值,

* 判断是否需要继续滑动。

*

* scrollTo(currX,currY):滑动到上面计算出的新的currX和currY位置处

*

* postInvalidate():通知View树重绘,作用和invalidate()方法一样

*/

@Override

public void computeScroll() {

if(mScroller.computeScrollOffset()){

int currX = mScroller.getCurrX();

int currY = mScroller.getCurrY();

Log.d("cylog", "滑动坐标"+"("+getScrollX()+","+getScrollY()+")");

scrollTo(currX, currY);

postInvalidate();

}

}

}

(3)MainActivity:这里主要执行布局的初始化以及监听按钮的点击事件:

public class MainActivity extends Activity {

private Button button;

private ParentView parentView;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

initView();

}

private void initView() {

button = (Button) findViewById(R.id.button);

parentView = (ParentView) findViewById(R.id.parentView);

button.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

//对parentView的内容进行滑动

parentView.smoothScrollTo();

}

});

}

}

(4)最后,我们看xml布局文件,这里要注意的是:我们引入了自定义布局,那么在xml布局就应该显式写出包名.类名,否则会出错,如下所示:

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent" >

android:id="@+id/button"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="开始下落"

android:layout_alignParentTop="true"

android:layout_centerHorizontal="true"

android:layout_marginTop="30dp" />

android:id="@+id/parentView"

android:gravity="center_horizontal"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:layout_below="@+id/button">

android:id="@+id/viewA"

android:layout_width="50dp"

android:layout_height="50dp"

android:layout_alignTop="@+id/view"

android:layout_alignRight="@+id/button"

android:layout_alignEnd="@+id/button"/>

完成了所有代码的编写后,运行测试,就会显示一开始说的效果了。

三、遇到的问题

笔者在学习Scroller的时候,由于Scroller涉及到了View的绘制原理,所以有时候会对View的重绘感到困惑,这里与大家分享我学习过程中遇到的一个问题。

在上一篇文章中,笔者有说到:在View#draw()方法中,绘制一个View有6个步骤,其中step 3中调用到onDraw()方法,说明一个View的重绘理论上是会调用到重写的onDraw()方法的,于是笔者在ParentView的onDraw()方法内打印了日志,看看是否真的会调用这个方法。但结果与分析不同,没有调用到onDraw()方法,为什么呢?经过查找了很多资料,终于知道了答案了。原来在一个View中,有这样一个方法:View#setWillNotDraw(boolean willNotDraw)

/**

* If this view doesn't do any drawing on its own, set this flag to

* allow further optimizations. By default, this flag is not set on

* View, but could be set on some View subclasses such as ViewGroup.

*

* Typically, if you override {@link #onDraw(android.graphics.Canvas)}

* you should clear this flag.

*

* @param willNotDraw whether or not this View draw on its own

*/

public void setWillNotDraw(boolean willNotDraw) {

setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);

}

从注释我们了解到,如果一个View不需要绘制任何内容,那么系统会对View的绘制进行优化,即不会调用到onDraw()方法,而系统判定是否需要进行优化的参数是willNotDraw。默认地,一个View继承了Viwe则这个参数设置为false,此时不优化;但一个ViewGroup默认会设置willNotDraw为true,即View树重绘的时候不会调用到ViewGroup的onDraw()方法。这也就解释了我的疑问,为什么在滑动的时候,进行了View树的重绘而ViewGroup的onDraw()方法始终没有调用。所以,如果要使ViewGroup的onDraw()方法得到调用,那么我们在实例化这个ViewGroup的时候应该调用这个方法:setWillNotDraw(false),设置不对ViewGroup进行优化,或者这样:为ViewGroup设置一个background属性(xml布局中),那么系统就会认为该ViewGroup存在内容了,此时就会每一次都调用onDraw()方法了。

解决了以上这个问题后,那么再引申出这样一个问题:ViewGroup重绘的时候,子View的onDraw()方法有没有调用呢?从理论上分析,我们在调用ViewGroup的重绘的时候是会调用到子View的draw()方法的,在draw()方法的内部又会调用onDraw()方法的,因此我们可以在子View的onDraw()方法内打印一下日志,事实上,在当前的Scroller背景下,子View的onDraw()方法是没有被调用的,但这个和上面说到的willNotDraw没有关系,因为子View是默认不开启优化的,那么到底为什么呢?其实在View的内部有一个标志参数,用来标志当前View是否需要重绘,如果这个View的内容没有改变,那么系统就会认为这个View不需要重新绘制,所以就不会调用子View的onDraw()方法了,由于当前的Scroller方法并没有对子View的内容作用,因此子View最终也没有调用这个onDraw()方法。以上为本人的一点见解,如果说错了,还望指正。还有,谢谢看到这里的你。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>