今天新鲜出炉的需求来了:产品要在首页上放置一个悬浮图标,这个图标既起着宣传的做用(图标上面有活动标题),也是一个按钮,点击以后能跳转到某个详情页面。并且为了用户体验更好,在滑动界面时,这个图标要乖乖地藏起来,不能影响用户操做。我仔细分析了一下,哟,这不就是中午点外卖时用的饿了么上面的购物车按钮么?java
用户没有触摸界面时,购物车就正常悬浮在右下角,当界面滑动时,它就自觉地将自身的一半缩到屏幕以外,并且会变得半透明,再也不遮挡底下的内容。到了这一步,相信你们都会想到是用触摸事件来实现了。android
@Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(event); }
触摸事件有三种,每一步的做用和实现的效果都不同:git
建立一个新项目,MainActivity的布局以下:github
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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" tools:context="com.lindroid.floatshoppingcart.MainActivity"> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent"> </ListView> <ImageView android:id="@+id/iv_cart" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_marginBottom="55dp" android:layout_marginRight="20dp" android:src="@mipmap/ic_shopping_cart" android:layout_width="50dp" android:layout_height="50dp" /> </RelativeLayout>
右下角的ImageView就是咱们的主角,图标是我本身找的购物车图标。为了模拟界面滑动,我在底下简单放了一个ListView,并填充了一些数据。app
public class MainActivity extends AppCompatActivity { private ListView listView; private ImageView ivCart; private List<String> titles = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); initView(); } private void initData() { for (int i = 0; i < 60; i++) { titles.add(new StringBuffer("这是一条数据").append(i).toString()); } } private void initView() { listView = (ListView) findViewById(R.id.listView); ivCart = (ImageView) findViewById(R.id.iv_cart); listView.setAdapter(new ArrayAdapter<>(this,android.R.layout.simple_list_item_1,titles)); } }
此时的效果以下:
布局写完,下面就来实现咱们想要的效果了。ide
悬浮按钮的动画效果很简单,就俩:布局
首先咱们要明确悬浮按钮的位移距离,以下图所示:优化
悬浮按钮的总位移等于它的右侧到右边屏幕的距离(蓝线),再加上它的半径(紫线)。半径咱们能够用getMeasuredWidth
获取它的宽度再除于2,那么蓝线的长度呢?动画
咱们没法直接获取控件右侧到右边屏幕的距离,可是咱们能够换个思路,先获取整个屏幕的宽度,再减去按钮右侧到左边的距离就好了,然后者可使用getRight轻松获得。获取手机屏幕宽高可使用下面的方法:this
private int[] getDisplayMetrics(Context context) { DisplayMetrics mDisplayMetrics = new DisplayMetrics(); ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(mDisplayMetrics); int W = mDisplayMetrics.widthPixels; int H = mDisplayMetrics.heightPixels; int array[] = {W, H}; return array; }
渐变更画就比较简单了,只须要设置起始透明度和结束透明度便可。
两种动画是同时开始和结束的,咱们能够设置一个动画集合:
private void hideFloatImage(int distance) { isShowFloatImage = false; //位移动画 TranslateAnimation ta = new TranslateAnimation( 0,//起始x坐标,10表示与初始位置相距10 distance,//结束x坐标 0,//起始y坐标 0);//结束y坐标(正数向下移动) ta.setDuration(300); //渐变更画 AlphaAnimation al = new AlphaAnimation(1f, 0.5f); al.setDuration(300); AnimationSet set = new AnimationSet(true); //动画完成后不回到原位 set.setFillAfter(true); set.addAnimation(ta); set.addAnimation(al); ivCart.startAnimation(set); }
动画发生以后不须要归位,因此记得setFillAfter
要设为true。
前面咱们讨论的都是界面滑动,按钮向右隐藏的动画,而用户的手指离开屏幕时,悬浮按钮是要回归原位,这时候的动画效果就跟以前的相反了,因此只需小小修改一下参数便可:
private void showFloatImage(int distance) { isShowFloatImage = false; //位移动画 TranslateAnimation ta = new TranslateAnimation( distance,//起始x坐标 0,//结束x坐标 0,//起始y坐标 0);//结束y坐标(正数向下移动) ta.setDuration(300); //渐变更画 AlphaAnimation al = new AlphaAnimation(1f, 0.5f); al.setDuration(300); AnimationSet set = new AnimationSet(true); //动画完成后不回到原位 set.setFillAfter(true); set.addAnimation(ta); set.addAnimation(al); ivCart.startAnimation(set); }
注意一下这里的位移动画的起始坐标。因为补间动画的特性,动画发生位移以后,移动的只是控件的内容,而不是控件自己,因此咱们要以控件所在位置为坐标原点,而不是发生位移后的内容!故这里的起始坐标是水平移动的距离,结束坐标是回到坐标原点,也就是0。
分析完动画效果以后,咱们如今就要来调用了,前面已经说过了是在触摸事件中监听,那么好的,咱们如今就将触摸事件和动画的代码集合起来吧:
private float startY; @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startY = event.getY(); break; case MotionEvent.ACTION_MOVE: if (Math.abs(startY - event.getY()) > 10) { hideFloatImage(moveDistance); } startY = event.getY(); break; case MotionEvent.ACTION_UP: new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { runOnUiThread(new Runnable() { @Override public void run() { showFloatImage(moveDistance); } }); return false; } }).sendEmptyMessageDelayed(0, 1500); break; } return super.dispatchTouchEvent(event); }
手指按下时,咱们只须要获取到起始坐标。这里要注意的是咱们的手指在手机屏幕上的触摸是一个面(手指的与屏幕的接触面积)而不只仅是一个点,因此MotionEvent.ACTION_MOVE
是很容易就触发的。为了不用户手指一按下悬浮按钮就移动,咱们能够设置一个值,当手指滑动的距离超过它时才视为有效的滑动。手指抬起时则延迟1.5s再让悬浮图案还原。固然,不要忘了动画是要主线程中进行的。
运行一下,就能够看到以下的效果了:
到如今咱们差很少实现了咱们想要的效果了,可是若是你试着快速滑动一下就会发现一个可怕的问题:
频繁滑动时动画就会频繁地触发,甚至你快速滑动几回后,悬浮按钮看起来就像抽了风同样。这显然是不行的,咱们接下来就作以下的优化:
这个咱们只须要加一个布尔值isShowFloatImage
来控制便可。每次调用动画判断一下。
以前是经过Handler发送延迟消息来执行动画的,这样没法控制动画的停止。那么如今,咱们就须要用另一种方法来控制显示动画了。这里我选择了Java的计时器Timer
。当用户的手指抬起时,咱们记下当前时间upTime
,下次用户再次按下手指时将当前时间与upTime
比较,差值小于1.5s的话则停止动画。
完成上面两步优化以后的代码以下:
private Timer timer; /**用户手指按下后抬起的实际*/ private long upTime; @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (System.currentTimeMillis() - upTime < 1500) { //本次按下距离上次的抬起小于1.5s时,取消Timer timer.cancel(); } startY = event.getY(); break; case MotionEvent.ACTION_MOVE: if (Math.abs(startY - event.getY()) > 10) { if (isShowFloatImage){ hideFloatImage(moveDistance); } } startY = event.getY(); break; case MotionEvent.ACTION_UP: if (!isShowFloatImage){ //开始1.5s倒计时 upTime = System.currentTimeMillis(); timer = new Timer(); timer.schedule(new FloatTask(), 1500); } break; default: break; } return super.dispatchTouchEvent(event); } class FloatTask extends TimerTask { @Override public void run() { runOnUiThread(new Runnable() { @Override public void run() { showFloatImage(moveDistance); } }); } }
再次运行一下,就会发现动画不会频繁地触发,比以前的体验更好了。