Android编程权威指南总结(十一)

第三十一章      定制视图与触摸事件

一、创建定制视图

      简单视图:简单视图内部也可以很复杂,之所以归为简单类别,是因为简单视图不包括子视图。简单视图几乎总是用来处理定制绘制。

      聚合视图:聚合视图由其他视图对象组成。聚合视图通常用来管理子视图,但不负责处理定制绘制。图形绘制任务都委托给了各个子视图。

      以下为创建定制视图所需的三大步骤:

  • 选择超类。对于简单定制视图而言,View是个空白画布,因此它作为超类最常见。对于聚合定制视图,我们应选择合适的超类布局,比如FrameLayout
  • 继承选定的超类,覆盖超类的构造方法。
  • 覆盖其他关键方法,以定制视图行为。

二、处理触摸事件

      监听触摸事件的一种方式是使用以下View方法,设置一个触摸事件监听器:public void setOnTouchListener(View.OnTouchListener l)。其工作方式与setOnClickListener(View.OnClickListener)相同。实现View.OnTouchListener接口,供触摸事件发生时调用。不过,由于定制视图是View的子类,也可走捷径直接覆盖以下View方法:public boolean onTouchEvent(MotionEvent event)。该方法接收一个MotionEvent类实例。MotionEvent类可用来描述包括位置和动作的触摸事件。动作用于描述事件所处的阶段。

      在onTouchEvent(...)实现方法中,可使用以下MotionEvent方法查看动作值:public final int getAction()。

1、跟踪运动事件

      在屏幕上绘制矩形框:首先,要知道定义矩形框的两个坐标点——原始坐标点(手指的初始位置),当前坐标点(手指的当前位置)。其次,定义一个矩形框,还需追踪记录来自多个MotionEvent的数据。可以新建一个Bean类,比如Box类,用于表示一个矩形框的定义数据。

public class Box { 
    private PointF mOrigin;
    private PointF mCurrent;
    
    public Box(PointF origin) {
        mOrigin = origin;
        mCurrent = origin;
    }
 
    public PointF getCurrent() {
        return mCurrent;
    }
 
    public void setCurrent(PointF current) {
        mCurrent = current;
    }
 
    public PointF getOrigin() {
        return mOrigin;
    }
}

      之后,可以在监听方法中获取当前手指触摸点的数据,设置给Box,并将Box对象添加到集合中。

      任何时候,只要接收到ACTION_DOWN动作事件,就以事件原始坐标新建Box对象并赋值给mCurrentBox,然后再添加到矩形框数组中。

      用户手指在屏幕上移动时,mCurrentBox.mCurrent会得到更新。在取消触摸事件或用户手指离开屏幕时,清空mCurrentBox以结束屏幕绘制。已完成的Box会安全地存储在数组中,但它们再也不会受任何动作事件影响了。

      注意ACTION_MOVE事件发生时要调用invalidate()方法。该方法会强制View重新绘制自己。这样,用户在屏幕上拖曳时就能实时看到矩形框。

@Override 
public boolean onTouchEvent(MotionEvent event) { 
    //X和Y坐标已经封装到PointF对象中,我们需要同时传递这两个坐标值,PointF类刚好能满足要求
    PointF current = new PointF(event.getX(), event.getY()); 
    String action = ""; 
    switch (event.getAction()) { 
        case MotionEvent.ACTION_DOWN: 
            action = "ACTION_DOWN"; 
            mCurrentBox = new Box(current);
            mBoxen.add(mCurrentBox);
            break; 
        case MotionEvent.ACTION_MOVE: 
            action = "ACTION_MOVE"; 
            if (mCurrentBox != null) {
                mCurrentBox.setCurrent(current);
                invalidate();
            }
            break; 
        case MotionEvent.ACTION_UP: 
            action = "ACTION_UP"; 
            mCurrentBox = null;
            break; 
        case MotionEvent.ACTION_CANCEL: 
            action = "ACTION_CANCEL"; 
            mCurrentBox = null;
            break; 
    } 
    //手指在屏幕触摸,可以得到触摸点的x、y坐标
    Log.i(TAG, action + " at x=" + current.x + ", y=" + current.y); 
    return true; 
}

三、onDraw(...)方法内的图形绘制

      Android调用顶级View视图的draw()方法,将视图绘制到屏幕上。这会引起自上而下的链式调用反应。首先,视图完成自我绘制,然后是子视图的自我绘制,再然后是子视图的子视图的自我绘制,如此调用下去直至继承结构的末端。当继承树中的所有视图都完成自我绘制后,最顶级View视图也就生效了。覆盖View的方法:protected void onDraw(Canvas canvas)。

      CanvasPaintAndroid系统的两大绘制类。

  • Canvas类拥有我们需要的所有绘制操作。其方法可决定绘在哪里以及绘什么,比如线条、圆形、字词、矩形等。
  • Paint类决定如何绘制。其方法可指定绘制图形的特征,例如是否填充图形、使用什么字体绘制、线条是什么颜色等。

      在自定义View的构造方法里实例化Paint对象。之后,重写onDraw()方法:

@Override 
protected void onDraw(Canvas canvas) { 
    // Fill the background 
    canvas.drawPaint(mBackgroundPaint); 
    for (Box box : mBoxen) { 
        float left = Math.min(box.getOrigin().x, box.getCurrent().x); 
        float right = Math.max(box.getOrigin().x, box.getCurrent().x); 
        float top = Math.min(box.getOrigin().y, box.getCurrent().y); 
        float bottom = Math.max(box.getOrigin().y, box.getCurrent().y); 
        canvas.drawRect(left, top, right, bottom, mBoxPaint); 
    } 
}

      以上代码的第一部分简单直接:使用米白背景paint,填充canvas以衬托矩形框。

      然后,针对矩形框数组中的每一个矩形框,据其两点坐标,确定矩形框上下左右的位置。绘制时,左端和顶端的值作为最小值,右端和底端的值作为最大值。完成位置坐标值计算后,调用Canvas.drawRect(...)方法,在屏幕上绘制红色矩形框。

挑战练习一:设备旋转问题

第一种:

Activity中添加:

@Override
protected void onSaveInstanceState(Bundle outState) {
    Log.e(TAG,"onSaveInstanceState");
    outState.putSerializable("save_data", (Serializable) mBoxen);
    super.onSaveInstanceState(outState);
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    Log.e(TAG,"onRestoreInstanceState");
    Serializable save_data = savedInstanceState.getSerializable("save_data");
    mBoxen = (List<Box>) save_data;
    super.onRestoreInstanceState(savedInstanceState);
}

第二种:

BoxDrawingView中添加:

@Nullable
@Override
protected Parcelable onSaveInstanceState() {
    Log.e(TAG,"onSaveInstanceState");
    Bundle bundle = new Bundle();
    Parcelable parcelable = super.onSaveInstanceState();
    bundle.putParcelable("super_data", parcelable);
    bundle.putSerializable("save_data", (Serializable) mBoxen);
    return bundle;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
    Log.e(TAG,"onRestoreInstanceState");
    Bundle bundle = (Bundle) state;
    Parcelable superData = bundle.getParcelable("super_data");
    mBoxen = (List<Box>) bundle.getSerializable("save_data");
    super.onRestoreInstanceState(superData);
}

注意:使用第二种方法,必须在layout布局文件中,给View添加上ID,不然上面那两个方法不会被调用到。

这两种方法都可以保存状态,但是:如果画的方框比较长,横屏过来就会显示不全。不知道是需要横竖屏的布局适配还是怎样。可以研究研究。

 

第三十二章     属性动画

一、属性动画实现原理

      ObjectAnimator 是个属性动画制作对象。要获得某种动画效果,传统方式是设法在屏幕上移动视图,而属性动画制作对象却另辟蹊径:以一组不同的参数值反复调用属性设置方法。

      调用以下方法可以创建ObjectAnimator对象:ObjectAnimator.ofFloat(mSunView, "y", 0, 1)。

      新建ObjectAnimator一旦启动,就会以从0开始递增的参数值反复调用mSunView. setY(float)方法:mSunView.setY(0);  mSunView.setY(0.02);  mSunView.setY(0.04); mSunView.setY(0.06); .......   直到调用mSunView.setY(1)为止。这个0~1区间参数值的确定过程又称为interpolation。可以想象到,在这个interpolation过程中,即便很短暂,确定相邻参数值也是要耗费时间的;由于人眼的视觉暂留现象,动画效果就形成了。

二、视图属性转换

      想让视图动起来的话,仅仅靠属性动画制作对象是不切实际的,尽管它确实很有用。因此,有了属性转换(transformation properties)。

      视图都有local layout rect(视图实例化时被赋予的位置及大小尺寸参数值)。知道了视图属性值(local layout rect),就可以改变这些属性值,从而实现四处移动视图。这种做法就叫做属性转换。例如,利用 rotationpivotX 和 pivotY 这三个参数可以旋转视图(见图32-3);利用 scaleX 和 scaleY 可以缩放视图(见图32-4);而利用 translationX 和 translationY 可以四处移动视图(见图32-5)。

      视图的所有这些属性值都有 getter 方法和 setter 方法。例如,调用 getTranslationX() 方法就能得到 translationX 值,调用 setTranslationX(float) 方法就能设置 translationX 值。那么 属性有什么作用呢?实际上,和 y 属性是以布局坐标为参考值设立的一种便利开发的属性值。例如,简单写几行代码,就可以把视图置于某个 和 Y 坐标确定的位置。分析其背后原理可知,这就是通过修改 translationX 和 translationY 属性值来实现的。所以,调用 mSunView.setY(50) 方法就等同于:mSunView.setTranslationY(50 - mSunView.getTop())。

三、使用不同的 interpolator

      TimeInterpolator 作用:改变 A 点到 B 点的动画效果。使用不同的TimeInterpolator对象可实现不同的动画特效。

objectAnimator.setInterpolator(new AccelerateInterpolator());

四、色彩渐变

      在 colors.xml 文件中定义颜色资源并存入到相应的实例变量。再添加一个 ObjectAnimator,实现色彩从 mBlueSkyColor 到 mSunsetSkyColor 变换的动画效果

ObjectAnimator sunsetSkyAnimator = ObjectAnimator
                .ofInt(mSkyView, "backgroundColor", mBlueSkyColor, mSunsetSkyColor)
                .setDuration(3000);
sunsetSkyAnimator.start();

      但是这样的色彩变幻不自然,仔细分析,颜色 int 数值并不是一个简单的数字,它实际是由四个较小数字转换而来。因此,只有知道颜色的组成奥秘,ObjectAnimator 对象才能合理地确定蓝色和橘黄色之间的中间值。不过,知道如何确定颜色中间值还不够,ObjectAnimator 还需要一个 TypeEvaluator 子类的协助。TypeEvaluator 能帮助 ObjectAnimator 对象精确地计算开始到结束间的递增值。Android 提供的这个 TypeEvaluator 子类叫作 ArgbEvaluator 。

sunsetSkyAnimator.setEvaluator(new ArgbEvaluator());

五、播放多个动画

      有时,你需要同时执行一些动画。这很简单,同时调用start()方法就行了。但是,假如要像编排舞步那样编排多个动画的执行,事情就没那么简单了。例如,为实现完整的日落景象,太阳落下去之后,天空应该从橘黄色再转为午夜蓝。办法总是有的,你可以使用 AnimatorListener。AnimatorListener 会让你知道动画什么时候结束。这样,执行完第一个动画,就可以接力执行第二个夜空变化的动画。然而,理论分析很简单,实际去做的话,少不了要准备多个监听器,这也很麻烦。好在 Android 还设计了方便又简单的 AnimatorSet

      首先,删除掉原来的动画启动代码,并添加夜空变化的动画代码:

ObjectAnimator nightSkyAnimator = ObjectAnimator
                .ofInt(mSkyView, "backgroundColor", mSunsetSkyColor, mNightSkyColor)
                .setDuration(1500);
nightSkyAnimator.setEvaluator(new ArgbEvaluator());

      然后,创建并执行一个动画集:

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(heightAnimator)
           .with(sunsetSkyAnimator)
           .before(nightSkyAnimator);
animatorSet.start();

      说白了,AnimatorSet 就是可以放在一起执行的动画集。可以用好几种方式创建动画集,但使用上述代码中的 play(Animator) 方法最容易。

      调用 play(Animator) 方法之前,要先创建一个 AnimatorSet.Builder 对象,然后利用它创建链式方法调用。传入 play(Animator) 方法的 Animator 是链首。所以,以上代码中的链式调用就可以这样解读:协同执行 heightAnimator 和 sunsetSkyAnimator 动画,在 nightSkyAnimator 之前执行 heightAnimator 动画。在实际开发中,可能会用到更复杂的动画集。这也没问题,需要的话,可以多次调用 play(Animator) 方法。

六、转场

      Android 4.4 引入了新的视图转场框架。从一个 activity 小视图动态弹出另一个放大版 activity 视图就可以使用转场框架实现。实际上,转场框架的工作原理很简单:定义一些场景,它们代表各个时点的视图状态,然后按照一定的逻辑切换场景。场景在XML布局文件中定义,转场在XML动画文件中定义。

      activity 已经运行的情况就不太适合使用转场框架,所以我们用了强大的属性动画框架。如果想实现以弹窗展示放大版图片这样的动画效果,首先要知道照片放在哪里,其次是如何在对话框里布置新图片。显然,对于这类布局动态转场任务,转场框架比 ObjectAnimator 更能胜任。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值