本章内容是在学些《Android开发艺术与探索》所做的一些笔记,主要是对学过知识的总结。
对于View的滑动,在Android开发中经常使用,比如下拉刷新,SlidingMenu等都用到了View的滑动。并且想要实现绚丽的自定义控件,View的滑动也是必不可少的知识。实现View的滑动主要有三种方式:第一种是通过View本身提供的scrollTo/scrollBy方法;第二种是通过动画给View施加平移效果实现滑动;第三种是通过改变View的LayoutParams使得View重新布局从而实现滑动。
对于View的滑动我们会做一些小Demo,Demo实现的功能很简单,就是在每点击按钮的时候让图片在屏幕上x和y轴都滑动50。
public void scrollTo(int x, int y)
public void scrollBy(int x, int y)
scrollTo实现了基于所传递参数的绝对滑动,而scrollBy其实内部也调用了scrollTo方法,实现了基于当前位置的相对滑动。对于View的滑动,有两个比较重要的属性mScrollX和mScrollY,这两个属性可以通过getScrollX和getScrollY获得。在滑动过程中,mScrollX值总是等于View的左边缘与View的内容的左边缘在水平方向的距离,而mScrollX的值总是等于View的上边缘与View内容的上边缘在竖直方向上的距离。并且当View左边缘在View内容左边缘的右边时,mScrollX为正值反之为负值,mScrollY同理。换句话说,如果从左向右滑动,那么mScrollX为负值,反之为正值,mScrollY同理。
注意:scrollTo/scrollBy只能改变View内容的位置而不能改变View在布局中的位置。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<FrameLayout
android:id="@+id/mFrameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/mImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
FrameLayout>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="imgClick"
android:text="X和Y都平移50"
android:layout_centerHorizontal="true"/>
RelativeLayout>
package com.wangjian.wjsliderdemo;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;
public class MainActivity extends Activity {
private FrameLayout mFrameLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mFrameLayout = (FrameLayout) findViewById(R.id.mFrameLayout);
}
/**
* 按钮的点击事件
* @param view
*/
public void imgClick(View view){
mFrameLayout.scrollTo(mFrameLayout.getScrollX()-50,mFrameLayout.getScrollY()-50);
}
}
由于scrollTo/scrollBy是让内容移动,所以我们想让图片移动,所以操作的是帧布局(图片就是帧布局的内容)。并且你会发现我们进行帧布局内容的移动的时候,使用的代码是这样的:
mFrameLayout.scrollTo(mFrameLayout.getScrollX()-50,mFrameLayout.getScrollY()-50);
如果直接写为mFrameLayout.scrollTo(-50,-50);那么只会移动一次,而不会点击一次移动一次。因为每次移动都是以它开始的位置为基准的(如果改为scrollBy是可以直接这样写的),其实这也是scrollTo和scrollBy的区别。并且想要让图片向右下角移动,应该传入的是-50而不是50。
运行效果图如下:
我们发现上面的滑动效果并不好看,比较生硬。而想要实现弹性滑动,需要使用Scroller。其实弹性滑动的实质就是将一次大的滑动划分成若干次小的滑动并在同一时间段完成。
想要比较熟练的使用弹性滑动,你首先应该了解一下三个方法(可以先看后面代码实现,在回过头来看理论知识):
public boolean computeScrollOffset() :Scroller类中的方法
public void startScroll(int startX, int startY, int dx, int dy, int duration) :Scroller类中的方法
public void computeScroll() : View中的方法
computeScrollOffset方法判主要是断滚动是否还在继续,Scroller类中的mFinished属性与之对应。如果滚动完成了,那么computeScrollOffset方法返回false,mFinished为true;如果滚动没有完成那么computeScrollOffset方法返回true,mFinished为false。
startScroll主要是用来开启滚动,startX和startY是滑动的起点,dx和dy是滑动的距离,duration是滑动时间。并且必须注意的是,仅仅调用startScroll方法是无法让View开始滑动的,因为它内部并没有做相关的滑动,仅仅是保存了我们传递的几个参数。那么View到底要怎么滑动呢?其实就是在调用该方法时,应该再调用invalidate方法,即重绘。重绘的时候则会导致View中的draw方法执行,draw方法中又会去调用computeScroll方法。遗憾的是computeScroll方法是一个空实现,因此需要我们自己去实现该方法。但是应该注意的是,在computeScroll方法中,我们应该调用View的postInvalidate方法来进行第二次重绘,和第一次重绘一样,也是为了导致computeScroll方法被调用,如此反复,知道滑动结束。
computeScroll方法也比较容易理解,它的代码格式一般都是固定的,主要是判断滑动是否完成,如果没有完成就去向Scroller获取当前的scrollX和scrollY,然后通过scrollTo实现滑动。最后再调用postInvalidate来让滑动持续进行。
最后总结一下:
首先startScroll开启滑动,并调用invalidate方法进行重绘。当View重绘后会在draw方法中调用computeScroll,而computeScroll又去向Scroller获取当前的scrollX和scrollY,然后通过scrollTo实现滑动。接着又调用postInvalidate进行第二次重绘,和第一次重绘一样,也是为了导致computeScroll方法被调用。然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo滑动到最新的位置,如此反复,知道整个滑动结束。
然后对上面的代码进行一些修改:
由于computeScroll是View中的方法,所以我们自定义View并重写computeScroll方法,代码如下:
package com.wangjian.wjsliderdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.widget.Scroller;
/**
* Created by Administrator on 2016/1/18.
*/
public class MyFrameLayout extends FrameLayout{
Scroller mScroller;
public MyFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
if(mScroller == null){
mScroller = new Scroller(context);
}
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
/**
* 开始滑动
*/
public void startMove(){
mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),-50,-50,1000);
invalidate();
}
}
代码很简单,大概使用流程就是,具体的实现过程上面已经说得比较详细:
①创建Scroller对象并调用它的startScroll,不过startScroll方法并不能实现滑动,因为他只是保存了我们传递的几个参数。
②重写View的computeScroll方法实现平滑移动。
package com.wangjian.wjsliderdemo;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends Activity {
private MyFrameLayout mFrameLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mFrameLayout = (MyFrameLayout) findViewById(R.id.mFrameLayout);
}
/**
* 按钮的点击事件
* @param view
*/
public void imgClick(View view){
mFrameLayout.startMove();
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.wangjian.wjsliderdemo.MyFrameLayout
android:id="@+id/mFrameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/mImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
com.wangjian.wjsliderdemo.MyFrameLayout>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="imgClick"
android:text="X和Y都平移50"
android:layout_centerHorizontal="true"/>
RelativeLayout>
运行结果如下:
效果就是我们想要的平滑移动了。
动画本身就是一种弹性滑动,所以对于动画实现的滑动不去专门讨论瞬间滑动和弹性滑动。通过动画让View进行滑动,其实质就是让View进行平移,其主要是操作translationX和translationY属性。我们要做的效果和上面类似,不过是在2000毫秒内平移200。
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">
<translate
android:duration="2000"
android:fromXDelta="0"
android:fromYDelta="0"
android:interpolator="@android:anim/linear_interpolator"
android:toXDelta="200"
android:toYDelta="200"
/>
set>
package com.wangjian.wjsliderdemo;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
public class MainActivity extends Activity implements View.OnClickListener {
private ImageView mImageView;
private Animation animation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mImageView = (ImageView) findViewById(R.id.mImageView);
mImageView.setOnClickListener(this);
//加载动画
animation = AnimationUtils.loadAnimation(this, R.anim.translate);
}
/**
* 按钮的点击事件
* @param view
*/
public void imgClick(View view){
mImageView.startAnimation(animation);
}
/**
* 图片的点击事件
* @param v
*/
@Override
public void onClick(View v) {
Log.e("TAG","图片被点击了");
}
}
MainActivity也很简单,点击按钮图片慢慢滑动,点击图片打印日志。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/mImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:onClick="imgClick"
android:text="X和Y都平移200" />
RelativeLayout>
我们发现当我们第一次点击图片时,打印了日志。但是当图片滑动到新的位置后,点击图片没有打印日志,反而在图片刚开始的位置点击却打印了日志。总结如下:
使用动画来对View进行滑动,只是对它的影像做操作,它并不能改变View的位置参数,包括宽高,它的真身依然在它开始的位置,所以点击图片新的位置并不能触发点击事件。对于这种问题,有两个解决方案:
①使用属性动画,但是要注意兼容性,这里只是提供让动画移动的方法,感兴趣的可以自己写代码尝试。
ObjectAnimator.ofFloat(mImageView,”translationX”,0,200).setDuration(2000).start();
上面这段代码可以在2000毫秒内让图片水平移动200,如果想让它往右下角移动,那么可以再加上:
ObjectAnimator.ofFloat(mImageView,”translationY”,0,200).setDuration(2000).start();
②预先在新位置创建一个和原来图片一模一样的图片,并设置相同的点击事件,其实就是模仿,也可以解决上面的问题。
并且通过属性动画能实现一些平常动画不能实现的效果,因为属性动画有一个比较重要的监听就是AnimatorUpdateListener,可以获取每一帧动画到来时动画完成的比例,我们可以根据比例计算出要滑动的距离,并滑动。并且除了滑动,还可以做很多其他的事情。
通过改变布局参数的方式来让View滑动,主要是设置View的LayoutParams里的margin或者padding属性。比如要将一个View向有滑动100,可以有以下两种思路:
①只需要将这个ViewLayoutParams里的marginLeft参数的值增加100即可。
②在VIew的左边放置一个空View1,默认宽度为0,只要不断加大View1的宽度,即可实现View向右移动。(当然这样做的前提是View的父容器必须是水平的LinearLayout)。
那么要怎么设置一个View的LayoutParams呢?还是使用上面的Demo,具体代码如下:
package com.wangjian.wjsliderdemo;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
public class MainActivity extends Activity {
private ImageView mImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mImageView = (ImageView) findViewById(R.id.mImageView);
}
/**
* 按钮的点击事件
* @param view
*/
public void imgClick(View view){
//获取mImageView的LayoutParams
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mImageView.getLayoutParams();
//设置左外边距和上外边距都加50
params.leftMargin += 50;
params.topMargin += 50;
//底下这两个方法都可以导致重新布局,从而引起滑动
mImageView.requestLayout();
mImageView.setLayoutParams(params);
}
}
布局文件基本没改,所以就不贴了。代码中的解释已经比较清楚了,也不多阐述。并且这种方式进行的修改,是将View的真身进行了改变,是永久性的。
运行效果:
可以通过使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。
对于Handler方法,搭配scrollTo/scrollBy或者LayoutParams都是可以实现弹性滑动的。通过发送延迟消息就可以实现。当然其他两种方式也是可以的。
具体不做代码演示。