View的基础知识
View的重要性我就不怎么强调了,虽然不是4大组件之一,但我们每一个app如果没有View,那么也就相当于失去了与用户的交互。
什么是View
View是android中所有控件的基类。就是这么牛逼!
我们可以看到连ViewGroup都是继承于View的可见View的逼格是如此之高。所以说不管是TextView之类的控件还是LinearLayout之类的布局都是继承于View的。
View的位置参数
View的位置参数主要由它的四个顶点,分别对应与View的四个属性:top(左上纵坐标),left(左上横坐标),right(右下横坐标),bottom(右下纵坐标)。这些坐标都是相对于这个View的父容器来说的,所以并不是绝对坐标,而是一种相对坐标。
根据这个我们能很轻松地得到一个View的宽高:
width = right - left;
height = top - bottom;
获得一个组件的四个属性方法如下:
left = View.getLeft()
right = View.getRight()
top = View.getTop()
bottom = View.getBottom()
大家在测试代码的时候最好把这几句话放在自己开的线程然后延迟一会儿里面测试,因为如果直接放在onCreate()里面由于View还没有绘制完毕,导致4个值都为0。
还要注意一点:比如我设置一个TextView宽度为40dp,那么在实际代码中运行出来的宽度应该是40*dpi/160。这个值才是像素。不同手机的dpi不同,大家可以自己试着算一算。
在Android3.0之后新加入了x,y,translationX,translationY四个参数。xy表示左上角的坐标(同样也是相对于父布局),translationX,translationY表示相对于父布局的偏移量(默认是0)。记一下下面两个公式:
x = left + translationX
y = top + translationY
用文字理解来说就是,当View指定了translationX之后View会水平平移。此时left表示原始左上角信息不会改变,而x表示的是当前View的左上角坐标。对于y来说也是一样。
MotionEvent和TouchSlop
1 .motionEvent
手指触碰屏幕事件。典型的事件类型有以下几种:
- ACTION_DOWN——手指刚接触屏幕
- ACTION_MOVE——手指在屏幕上滑动
- ACTION_UP——手指从屏幕上松开的一瞬间
轻触屏幕:ACTION_DOWN->ACTION_UP
滑动屏幕:ACTION_DOWN-> ACTION_MOVE->ACTION_UP
通过MotionEvent对象我们可以获得点击事件发生的xy坐标。getX/getY(返回相对于当前View左上角的xy坐标)与getRawX/getRawY(返回相对于手机屏幕左上角的xy坐标)。
2.TouchSlop
TouchSlop是系统能够识别出的被认为是滑动的最小距离。可通过ViewConfiguration.get(context).getScaledTouchSlop()
来获得。这个常量表示的意思是,如果用户在手机上滑动的距离小于这个值,那么手机就不把这个动作当做是滑动。TouchSlop这个常量因手机而异
VelocityTracker,GestureDetector和Scroller
1.VelocityTracker(速度追踪)。
可通过以下代码调用:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(motionEvent);
velocityTracker.computeCurrentVelocity(10000);
int X = (int) velocityTracker.getXVelocity();
int Y = (int) velocityTracker.getYVelocity();
第一行大概就是获得一个VelocityTracker 对象,
第二行大概就是对于motionEvent进行我们的速度追踪(相当于绑定)。前两行书中没有说明,所以我写的只是我的猜测。
第三行表示如果我们滑动的速度是100像素/1000ms,那么由于我们填入的是10000ms,所以最后XY显示的值为我们以100像素/1000ms的速度经过10000ms滑动的距离。简单点说就是把一个固定的速度值改变了单位,那么值自然也会改变。1米/秒=3600米/小时一个道理。希望我这样解释大家能看懂哈~
第四五行就是根据第三行填入的参数最后经过计算出的X Y方向的速度。
注意:
1.速度也有可能是负数因为计算公式是(结束坐标-开始坐标)/总时间,也就是说往左滑X是负的,上滑Y是负的
2.最后在不需要使用它的时候记得调用clear方法回收内存
velocityTracker.clear();
velocityTracker.recycle();
2.GestureDetector(手势检测)
用于辅助检测用户的单击,滑动,长按,双击等行为。在实现OnGestureListener接口之后会要求实现以下方法:
@Override
public boolean onDown(MotionEvent motionEvent) {
//手指触碰屏幕一瞬间,由一个ACTION_DOWN触发
return false;
}
@Override
public void onShowPress(MotionEvent motionEvent) {
//触摸之后触发,与onDown不同,onShowPress强调的是没有松开或拖动的状态
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
//手指轻轻触摸屏幕之后松开,由ACTION_UP触发
return true;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
//手指拖动,由一个ACTION_DOWN和多个ACTION_MOVE触发
return true;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
//手指长按屏幕
}
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
//手指快速滑动,由一个ACTION_DOWN,多个ACTION_MOVE和一个ACTION_UP触发
return true;
}
大家自己测试一下代码可以写个log看看调试信息。不过要把一些return false的改成return true,跟我上面写的那样。为什么?这个涉及到事件分发的问题,我们在下面会提到。
此外作者建议我们自己写代码来实现手势检测,通过比较XY坐标和一系列动作经过的时间,自己写相应的代码。
3.Scroller
弹性滑动对象,用于实现View的弹性滑动。书中说具体会在后面提到,那就让我们往下看吧。我们这里只需要知道Scroller能够用来实现滑动的过渡效果。
View的滑动
Android设备里面滑动效果说来大致分为三种方式:
- 通过View本身的scrollTo()/scrollBy()
- 通过动画给View施加平移效果
- 通过改变View的LayoutParams使得View重新布局
下面让我们一条一条学吧~
1.scrollTo()/scrollBy()
scrollTo():实现了基于所传递参数的绝对滑动。
scrollBy():调用了scrollTo(),实现了基于当前位置的相对滑动。
下面直接上代码然后大家可以自己试着观察一下效果:
<Button
android:id="@+id/test1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:onClick="test1"
android:text="scrollTo"
android:textAllCaps="false" />
<Button
android:id="@+id/test2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:onClick="test2"
android:text="scrollBy"
android:textAllCaps="false" />
public class MainActivity extends Activity {
Button button1;
Button button2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button1 = (Button) findViewById(R.id.test1);
button2 = (Button) findViewById(R.id.test2);
}
public void test1(View view) {
button1.scrollTo(20, 20);
}
public void test2(View view)
{
button2.scrollBy(20,20);
}
}
scrollTo按钮:我们会发现按钮里面的文字会向左上方偏移20像素,但继续按就没有效果了。
scrollBy按钮:一直按,文字会一直往左上角移动。
那其实也就是说scrollTo是只会根据原来的起始位置滑动,而scrollBy是根据当前的位置滑动。
注意:滑动的是View里面的内容,并不是View本身;左滑为正,右滑为负。
2.使用动画
书上还介绍了3.0一下的兼容动画,那我在这里就不赘述了(Android现在都不知道出到多少版本了)。童鞋们如果想了解animation或者属性动画的,可以在最后的TIPS里面找到链接,我在这里就不重复造轮子了呦~
这里就注意一点:一般的animation没有真正改变view控件的位置,而属性动画改变了,大家可以写个点击事件自己测试一下。
3.改变布局参数
这个方法就比较皮了,比方说我一个按钮左边放了一块空白的FrameLayout,然后让FrameLayout宽度变大,那么我这个按钮自然而然就会往右边移动;或者我们也可以直接把我们按钮的marginLeft+一点点数值,也能向右移动。
改变View的margin:
ViewGroup.MarginLayoutParams marginLayoutParams =(ViewGroup.MarginLayoutParams)test.getLayoutParams(); //获取test图像的布局参数
marginLayoutParams.leftMargin+=100;
test.requestLayout();//重新绘制test的布局
三种滑动方式的对比
- 只能滑动内容,不能滑动View本身
- 属性动画没什么问题,其他的会引起视觉错误(动画移到一个地方,但组件其实还在原地)
- 这个就麻烦点,而且挤压的方式我自己觉得不怎么好,改自身View的参数也是可以的
书中介绍了一个小项目,手指拖动一个View在屏幕上滑来滑去。有兴趣的朋友们可以看看,书中用的是兼容android3.0以下的nineoldandroids,使用ViewHelper这个类。我在这里就用objectAnimator来实现这个效果吧
package com.example.testapp;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.widget.ImageView;
public class MainActivity extends Activity {
ImageView test;
float LastX,LastY;//记录手指抬起以后的View最后的位置
float initX,initY;//手指触碰到屏幕时的初始化位置
float deltaX, deltaY;//每次移动的偏移量
int[] location = new int[2];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
test = (ImageView) findViewById(R.id.image_test);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (location[0] == 0 && location[1] == 0) {
test.getLocationInWindow(location);//由于不能在onCreate()里进行测量,所以我们在这里延迟初始化
location[0] += test.getWidth() / 2;//location获取的是View左上角坐标,我们分别加上View的宽/2,高/2,来使得我们拖动的是View的中心点而不是左上角
location[1] += test.getHeight() / 2;
}
initX = event.getRawX() - location[0];//按下事件的屏幕坐标-View的初始坐标=View所要偏移的量
initY = event.getRawY() - location[1];
ObjectAnimator objectAnimatorX1 = ObjectAnimator.ofFloat(test, "translationX", LastX, initX);//由于LastX,LastY没有被初始化,所以此时为0
ObjectAnimator objectAnimatorY1 = ObjectAnimator.ofFloat(test, "translationY", LastY, initY);//按钮按下的时候View从初始位置移动到手指位置
objectAnimatorX1.setDuration(0);
objectAnimatorY1.setDuration(0);
AnimatorSet animSet1 = new AnimatorSet();
animSet1.play(objectAnimatorX1).with(objectAnimatorY1);
animSet1.start();
break;
case MotionEvent.ACTION_MOVE:
deltaX = event.getRawX() - location[0];//拖动过程中每次监听到ACTION_MOVE时手指位置距离初始位置的偏移值
deltaY = event.getRawY() - location[1];
ObjectAnimator objectAnimatorX = ObjectAnimator.ofFloat(test, "translationX", LastX, deltaX);
ObjectAnimator objectAnimatorY = ObjectAnimator.ofFloat(test, "translationY", LastY, deltaY);
objectAnimatorX.setDuration(0);
objectAnimatorY.setDuration(0);
AnimatorSet animSet = new AnimatorSet();
animSet.play(objectAnimatorX).with(objectAnimatorY);
animSet.start();
break;
default:
break;
}
LastX = initX;//保存一个动作完成之后的最终状态
LastY = initY;
return true;
}
}
布局就简单的一个ImageView,我就不贴代码了。如果上面的属性动画看的不是很懂,建议看一下最后hongyang大神属性动画介绍。
弹性滑动
上面写的代码都是瞬间到达的,没有一点过渡,这样看起来没有美感。所以说弹性滑动我们也是要了解的。实现弹性滑动也有几种方式:
- Scroller
- 动画
- 延时策略
Scroller
说实话书上写的都是啥???反正我是没看懂,就介绍了一下源码,就说想必我们已经会使用了?(黑人问号脸???)。我还是老老实实网上自学去吧。下面是我自己学习后简单的代码:
public class MyScroller extends ViewGroup {
private Scroller scroller;
private float initX, initY;//手指按下时的屏幕坐标
private float LastX, LastY;//动作完成以后记录组件的最后一个位置
private float x, y;//move过程中的坐标
public MyScroller(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
scroller = new Scroller(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 为ScrollerLayout中的每一个子控件测量大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 为MyScroller中的每一个子控件在水平方向上进行布局
childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {//return true 保证点击事件是由我这个界面拦截掉
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initX = event.getRawX();
initY = event.getRawY();
LastX = initX;
LastY = initY;
break;
case MotionEvent.ACTION_MOVE:
x = event.getRawX();
y = event.getRawY();
float scrolledX = LastX - x;
float scrolledY = LastY - y;
scrollBy((int) scrolledX, (int) scrolledY);
LastX = x;
LastY = y;
break;
case MotionEvent.ACTION_UP:
// 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
scroller.startScroll(getScrollX(), getScrollY(), -getScrollX(), -getScrollY(), 2000);//getScrollX表示布局左上角坐标X-当前scroller的X
invalidate();//重新绘图
break;
}
return true;//保证消耗掉点击事件
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {//computeScrollOffset返回true表示滚动还没结束
scrollTo(scroller.getCurrX(), scroller.getCurrY());
invalidate();//重新绘图
}
}
}
<com.example.testapp.MyScroller
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="Scroller下面的子视图"
android:textAllCaps="false" />
</com.example.testapp.MyScroller>
大家最好先看后面的再来看这段代码,因为涉及到touch事件的分发机制。第一次看这种代码的同学们可能会有云里雾里的感觉,不过我注释尽量会多些一点。上述代码做出的效果是按下后平移在松开时按钮回到原来位置这个动画持续2秒。
动画
动画就是设置duration,很简单。嘿嘿~
延时策略
说白了就是把左移500,分成10次每次移动50,然后发送移动指令的时候加一个延时,上代码!
public class MainActivity extends Activity {
ImageView test;
LinearLayout linearLayout;
int x;
int count;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
test = (ImageView) findViewById(R.id.test);
linearLayout = (LinearLayout) findViewById(R.id.mainActivity);
}
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
if (count <= 10) {
x += 5;
linearLayout.scrollBy(x, 0);
count++;
handler.sendEmptyMessageDelayed(0, 20);
}
}
};
public void test(View view) {
handler.sendEmptyMessage(0);
}
}
相信大家一下子就能看懂,Handler相关的不会的话可以网上查一查,我这里是把linearLayout里面的一个ImageView向左平移了50像素。
哇,就很皮。这本书一章的内容好多啊。算了算了我还是分成两部分吧。
下一节我们讲View的分发机制~