仿餓了麼懸浮購物車按鈕
1、需求分析及思路分析
今天新鮮出爐的需求來了:產品要在首頁上放置一個懸浮圖示,這個圖示既起著宣傳的作用(圖示上面有活動標題),也是一個按鈕,點選之後能跳轉到某個詳情頁面。而且為了使用者體驗更好,在滑動介面時,這個圖示要乖乖地藏起來,不能影響使用者操作。我仔細分析了一下,喲,這不就是中午點外賣時用的餓了麼上面的購物車按鈕麼?
使用者沒有觸控介面時,購物車就正常懸浮在右下角,當介面滑動時,它就自覺地將自身的一半縮到螢幕之外,而且會變得半透明,不再遮擋底下的內容。到了這一步,相信大家都會想到是用觸控事件來實現了。
@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);
}
觸控事件有三種,每一步的作用和實現的效果都不一樣:
- 手指按下(ACTION_DOWN):使用者手指在螢幕上按下時,記下此時的y座標作為起始y座標(startY);
- 手指擡起(ACTION_UP):獲取此時的座標作為結束座標與
- 手指滑動(ACTION_MOVE):根據手指滑動的距離
2、專案建立及佈局編寫
建立一個新專案,MainActivity的佈局如下:
<?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,並填充了一些資料。
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));
}
}
此時的效果如下:
佈局寫完,下面就來實現我們想要的效果了。
3、懸浮按鈕的動畫效果
懸浮按鈕的動畫效果很簡單,就倆:
- 位移動畫:懸浮按鈕向右平移,直至一半在螢幕之外;
- 漸變動畫:懸浮按鈕在位移的同時逐漸變得半透明,此處將透明度設為0.5.
3.1 位移動畫
首先我們要明確懸浮按鈕的位移距離,如下圖所示:
懸浮按鈕的總位移等於它的右側到右邊螢幕的距離(藍線),再加上它的半徑(紫線)。半徑我們可以用
getMeasuredWidth
獲取它的寬度再除於2,那麼藍線的長度呢?
我們無法直接獲取控制元件右側到右邊螢幕的距離,但是我們可以換個思路,先獲取整個螢幕的寬度,再減去按鈕右側到左邊的距離就行了,而後者可以使用getRight輕鬆得到。獲取手機螢幕寬高可以使用下面的方法:
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;
}
3.2 漸變動畫
漸變動畫就比較簡單了,只需要設定起始透明度和結束透明度即可。
3.3 動畫集合
兩種動畫是同時開始和結束的,我們可以設定一個動畫集合:
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。
3.4 懸浮按鈕迴歸原位的動畫
前面我們討論的都是介面滑動,按鈕向右隱藏的動畫,而使用者的手指離開螢幕時,懸浮按鈕是要回歸原位,這時候的動畫效果就跟之前的相反了,所以只需小小修改一下引數即可:
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。
4、監聽手指觸控事件
分析完動畫效果之後,我們現在就要來呼叫了,前面已經說過了是在觸控事件中監聽,那麼好的,我們現在就將觸控事件和動畫的程式碼集合起來吧:
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再讓懸浮圖案還原。當然,不要忘了動畫是要主執行緒中進行的。
執行一下,就可以看到如下的效果了:
5、優化動畫效果
到現在我們差不多實現了我們想要的效果了,但是如果你試著快速滑動一下就會發現一個可怕的問題:
頻繁滑動時動畫就會頻繁地觸發,甚至你快速滑動幾次後,懸浮按鈕看起來就像抽了風一樣。這顯然是不行的,我們接下來就做如下的優化:
1. 當懸浮按鈕處於顯示狀態時,不會觸發顯示動畫,處於隱藏狀態時,不會觸發隱藏動畫;
2. 當手指按下擡起,延時執行顯示動畫的1.5s內,如果使用者再次按下擡起手指,則中止之前的動畫,並重新計算延遲時間。
優化1
這個我們只需要加一個布林值isShowFloatImage
來控制即可。每次呼叫動畫判斷一下。
優化2
之前是通過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);
}
});
}
}
再次執行一下,就會發現動畫不會頻繁地觸發,比之前的體驗更好了。