文章目录
1. View
和ViewGroup
的关系
View
是Android
所有控件的基类,我们平常用的布局控件 LinearLayout
,它继承自 ViewGroup
,ViewGroup
可以理解为 View
的组合,它可以包含很多 View
以及ViewGroup
。如下图:
需要注意的是ViewGroup
也继承自View
。ViewGroup
派生了多种布局控件子类,比如LinearLayout
、RelativeLayout
等。
2. 坐标获取
getTop()
,获取View
自身顶边到其父布局顶边的距离;getLeft()
,获取View
自身左边到其父布局左边的距离。getRight()
,获取View
自身右边到其父布局左边的距离。getBottom()
,获取View
自身底边到其父布局顶边的距离。View
获取自身宽度:width = getRight() - getLeft();
。当然系统提供了getWidth()
来直接获取;View
获取自身高度:height=getBottom() - getTop();
。同样的系统提供了getHeight()
来直接获取;- 触控事件中,使用
getRawX()
和触控事件中,使用getRawX()
和getRawY()
方法获得的坐标也是
Android
坐标系的坐标。getRawY()
方法获得的坐标也是Android
坐标系的坐标。 - 在触摸事件中,使用
onTouchEvent(MotionEvent event)
方法来处理,MotionEvent
也提供了获取焦点坐标的各种方法。 getX()
:获取点击事件距离控件左边的距离,即视图坐标。getY()
:获取点击事件距离控件顶边的距离,即视图坐标。getRawX()
:获取点击事件距离整个屏幕左边的距离,即绝对坐标。getRawY()
:获取点击事件距离整个屏幕顶边的距离,即绝对坐标。
3. View的滑动
View
的滑动是Android
实现自定义控件的基础,同时在开发中我们也难免会遇到View
的滑动处理。其实不管是哪种滑动方式,其基本思想都是类似的:当点击事件传到View
时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View
的坐标。实现View
滑动有很多种方法,在这里主要讲解6种滑动方法,分别是layout()
、offsetLeftAndRight()
与
offsetTopAndBottom()
、LayoutParams
、动画、scollTo
与 scollBy
,以及Scroller
。
3.1 案例layout()
首先自定义View
,如:
package com.example.myapplication;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
public class MyView extends View {
private int lastX;
private int lastY;
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 复写onTouchEvent,以使用坐标
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取手指坐标
int x = (int)event.getX();
int y = (int)event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetx = x - lastX;
int offsety = y - lastY;
// 使用layout方法来重新放置它的位置
layout(getLeft()+offsetx, getTop()+offsety, getRight()+offsetx, getBottom()+offsety);
break;
}
return true;
}
}
然后,在主布局文件中使用即可,如:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.myapplication.MyView
android:id="@+id/myView"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary"
/>
</LinearLayout>
效果:
拖动可移动。
3.2 案例offsetLeftAndRight & offsetTopAndBottom
case MotionEvent.ACTION_MOVE:
int offsetx = x - lastX;
int offsety = y - lastY;
// 使用offsetLeftAndRight & offsetTopAndBottom
offsetLeftAndRight(offsetx);
offsetTopAndBottom(offsety);
break;
3.3 案例LayoutParams(改变布局参数)
LayoutParams
主要保存了一个View
的布局参数,因此我们可以通过LayoutParams
来改变View
的布局参数从而达到改变View
位置的效果。
case MotionEvent.ACTION_MOVE:
int offsetx = x - lastX;
int offsety = y - lastY;
// 使用
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetx;
layoutParams.topMargin = getTop() + offsety;
setLayoutParams(layoutParams);
break;
这里使用LinearLayout.LayoutParams
的原因是主文件我们使用的布局是LinearLayout
。如果父控件是RelativeLayout
,则要使用RelativeLayout.LayoutParams
。还可以使用ViewGroup.MarginLayoutParams
来实现:
case MotionEvent.ACTION_MOVE:
int offsetx = x - lastX;
int offsety = y - lastY;
// 使用
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetx;
layoutParams.topMargin = getTop() + offsety;
setLayoutParams(layoutParams);
break;
3.4 动画
可以使用动画来进行移动。
布局文件不变,然后删除自定义控件中的onTouchEvent
方法,在res
文件下新建anim
文件夹,在其下新建translate.xml
文件,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true">
<translate
android:duration="1000"
android:fromXDelta="0"
android:toXDelta="300"/>
</set>
属性android:fillAfter="true"
用于设置运动后停留在该位置。
在MainActivity.java
中设置关联,即:
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MyAdapter adapter;
private List<String> mList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.recycleview);
MyView myView = findViewById(R.id.myView);
// 设置关联,加载定义的动画
myView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));
}
}
需要注意的是,View
动画并不能改变View
的位置参数。如果对一个Button
进行如上的平移动画操作,当Button
平移300
像素停留在当前位置时,我们点击这个Button
并不会触发点击事件,但在我们点击这个Button
的原始位置时却触发了点击事件。在Android 3.0
时出现的属性动画解决了上述问题:
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MyAdapter adapter;
private List<String> mList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.recycleview);
MyView myView = findViewById(R.id.myView);
// 从0变化到300
ObjectAnimator.ofFloat(myView, "translationX", 0, 300).setDuration(1000).start();
}
}
关于属性动画,这里做一个简单的介绍
3.4.1 属性动画
Android
中动画分类:
- 帧动画
View
动画,它提供了AlphaAnimation
、RotateAnimation
、TranslateAnimation
、ScaleAnimation
这4
种动画方式,并提供了AnimationSet
动画集合来混合使用多种动画。但不具有交互性,即:当某个元素发生View
动画后,其响应事件的位置依然在动画进行前的地方,所以View
动画只能做普通的动画效果,要避免涉及交互操。但是它的优点也非常明显:效率比较高,使用也方便。
作。- 属性动画,由
Android 3.0
推出。Animator
框架中使用最多的就是AnimatorSet
和ObjectAnimator
配合:使用ObjectAnimator
进行更精细化的控制,控制一个对象和一个属性值,而使用多个ObjectAnimator
组合到AnimatorSet
形成一个动画。属性动画通过调用属性get
、set
方法来真实地控制一个View
的属性值,因此,强大的属性动画框架基本可以实现所有的动画效果。
创建一个 ObjectAnimator
只需通过其静态工厂类直接返还一个ObjectAnimator
对象。参数包括一个对象和对象的属性名字,但这个属性必须有get
和set
方法,其内部会通过Java
反射机制来调用set
方法修改对象的属性值。
下面就是一些常用的可以直接使用的属性动画的属性值。
translationX
和translationY
:用来沿着X轴或者Y轴进行平移。rotation
、rotationX
、rotationY
:用来围绕View的支点进行旋转。PrivotX
和PrivotY
:控制View
对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认该支点
位置就是View
对象的中心点。alpha
:透明度,默认是1
(不透明),0
代表完全透明。x
和y
:描述View
对象在其容器中的最终位置。
如果一个属性没有get
、set
方法,也可以通过自定义一个属性类或包装类来间接地给这个属性增加get
和set
方法。如:
自定义view
:
public class MyView extends View {
private int lastX;
private int lastY;
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
对应布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.myapplication.MyView
android:layout_height="50dp"
android:layout_width="50dp"
android:id="@+id/myView"
android:background="@color/colorPrimary"
/>
</LinearLayout>
主文件:
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MyAdapter adapter;
private List<String> mList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyView myView = findViewById(R.id.myView);
PackageView packageView = new PackageView(myView);
ObjectAnimator.ofInt(packageView, "width", 300).setDuration(1000).start();
}
// 包装类
public class PackageView{
private View view;
PackageView(View view){
Log.d("HELLO", "PackageView: "+Boolean.toString(view==null));
this.view = view;
}
public int getWidth(){
return view.getLayoutParams().width;
}
public void setWidth(int width){
Log.d("HELLO", "setWidth: "+Boolean.toString(this.view==null));
view.getLayoutParams().width = width;
// 重新绘制View
view.requestLayout();
}
}
}
最终效果,宽度缓慢扩充到300
组合动画 AnimatorSet
上面的ObjectAnimator
针对单个属性动画,往往动画比较复杂,不单单涉及一个属性。就需要使用组合动画。
AnimatorSet.Builder
中包括以下4
个方法。
after
(Animator anim
):将现有动画插入到传入的动画之后执行。after
(long delay
):将现有动画延迟指定毫秒后执行。before
(Animator anim
):将现有动画插入到传入的动画之前执行。with
(Animator anim
):将现有动画和传入的动画同时执行。
如:
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MyAdapter adapter;
private List<String> mList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyView myView = findViewById(R.id.myView);
ObjectAnimator animator1 = ObjectAnimator.ofFloat(myView, "translationX", 300);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(myView, "scaleX", 1.0f, 2.0f);
AnimatorSet set = new AnimatorSet();
set.setDuration(1000);
set.play(animator1).after(animator2);
set.start();
}
}
和View
动画一样,属性动画也可以直接写在XML
中。
XML
中定制属性动画
前面提到了View
也可以指定XML
动画,但是其改变后点击事件还是在原来位置上,故而这里的XML
属性动画还是ObjectAnimator
版本。
同样的,在animator
中定义test.xml
文件,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duration="1000"
android:propertyName="scaleX"
android:valueFrom="1.0"
android:valueTo="3.0"
android:valueType="floatType"
>
</objectAnimator>
主文件:
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MyAdapter adapter;
private List<String> mList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyView myView = findViewById(R.id.myView);
myView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "Hello", Toast.LENGTH_SHORT).show();
}
});
Animator animator = AnimatorInflater.loadAnimator(this, R.animator.test);
animator.setTarget(myView);
animator.start();
}
}
3.5 scrollTo
与scollBy
scrollTo(x,y)
表示移动到一个具体的坐标点,而scrollBy(dx,dy)
则表示移动的增量为dx
、dy
。
case MotionEvent.ACTION_MOVE:
int offsetx = x - lastX;
int offsety = y - lastY;
// 使用
((View)getParent()).scrollBy(-offsetx, -offsety);
break;
内容参考:
- 《Android进阶之光》