今天新鮮出爐的需求來了:產品要在首頁上放置一個懸浮圖標,這個圖標既起着宣傳的做用(圖標上面有活動標題),也是一個按鈕,點擊以後能跳轉到某個詳情頁面。並且爲了用戶體驗更好,在滑動界面時,這個圖標要乖乖地藏起來,不能影響用戶操做。我仔細分析了一下,喲,這不就是中午點外賣時用的餓了麼上面的購物車按鈕麼?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); } }); } }
再次運行一下,就會發現動畫不會頻繁地觸發,比以前的體驗更好了。