Android 自定義View 之 可隨意拖動的View
因為趕專案本人停更兩個月 從今天開始又可以更新了 今天說一下這個可隨意拖動的view 簡單說一下這個view效果 和 發展 一開始這種效果是使用在網頁端的特別是購物類 例如某寶 某東 購物車和客服視窗 都有使用這個懸浮可拖動的設計效果 後來才發展到的移動端 還有手機桌面也是用到了這種效果 例如某族手機的訊息中心 手機桌面的懸浮球 某訊 和 某榮耀手遊 某吃雞遊戲 某視訊軟體等等也都是這種效果 這種方式的好處就是是可以隨時隨地的快速進去到你需要的頁面 目前來說 有很多的大型app選擇了這種互動方式 服務使用者 ;
下面說一下android 端的使用及實現
先看一下效果圖
編碼思路
首先實現這種效果的方式有兩種 一是自定義viewgroup 自定義容器 需要獲取手機懸浮窗許可權和處理各種事件 二 使用自定義view 第一種方式相對簡單一些 但是使用效果不好 所以我也就放棄掉了
今天主要說一下通過自定義view 的方式實現
自定義的主要思路 : 以ImageView為例
建立FreeView 繼承 ImageView
首先 獲取最大高度和寬度 設定view 的活動範圍 (主要考慮是否支援螢幕外 具體細節下面再說)
第二 通過onTouchEvent 觸控事件進行 位移計算 重置FreeView 的四角座標
第三 就是完成顯示
主要思路就這麼多 我的重點是第一步和第二步
注意
第一步 獲取最大高度和寬度 就是給定FreeView的活動範圍 最大寬度很容易獲取 需要注意的是最大高度 我們螢幕組成
window = statusbar + layout + navigationbar (從上倒下的順序)
受android碎片化影響 andorid手機的高度具有很多種不確定性 所以要做好高度的適配
還有第二個要注意的地方 這裡跟你的需求有關係 就是是否允許FreeView 出現在螢幕外 這種需求例如魅族手機的訊息中心 顆拖到螢幕邊緣 留下一個小尾巴 點選或者拖動時在展現給使用者
第二步 這裡主要是實現計算 和 實現方式 首先 onTouchEvent 通過觸控監聽MotionEvent.ACTION_DOWN 我可以得到點選時的座標 通過MotionEvent.ACTION_MOVE 可以得到離開時的座標 然後通過給FreeView設定layout 實現 位置重置 在這裡還要注意點選事件和拖動時間的衝突處理 程式碼中會有體現
import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.ImageView; import com.nade.commenlib.util.UiUtils; /** *Created by Nade on 2018/10/2. *隨意拖動的view */ @SuppressLint("AppCompatCustomView") public class FreeView extends ImageView { private int width; // 測量寬度 FreeView的寬度 private int height; // 測量高度 FreeView的高度 private int maxWidth; // 最大寬度 window 的寬度 private int maxHeight; // 最大高度 window 的高度 private Context context; private float downX; //點選時的x座標 private float downY; // 點選時的y座標 //是否拖動標識 private boolean isDrag=false; // 處理點選事件和滑動時間衝突時使用 返回是否拖動標識 public boolean isDrag() { return isDrag; } // 初始化屬性 public FreeView(Context context, AttributeSet attrs) { super(context, attrs); this.context=context; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 獲取屏寬高 和 可是適用範圍 (我的需求是可在螢幕內拖動 不超出範圍 也不需要隱藏) width=getMeasuredWidth(); height=getMeasuredHeight(); maxWidth = UiUtils.getMaxWidth(context); // maxHeight = UiUtils.getMaxHeight(context)-getStatusBarHeight();// 此時減去狀態列高度 注意如果有狀態列 要減去狀態列 如下行 得到的是可活動的高度 maxHeight = UiUtils.getMaxHeight(context)-getStatusBarHeight() - getNavigationBarHeight(); } // 獲取狀態列高度 public int getStatusBarHeight(){ int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); return getResources().getDimensionPixelSize(resourceId); } // 獲取導航欄高度 public int getNavigationBarHeight() { int rid = getResources().getIdentifier("config_showNavigationBar", "bool", "android"); if (rid!=0){ int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android"); return context.getResources().getDimensionPixelSize(resourceId); }else return 0; } /** * 處理事件分發 * @param event * @return */ @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); if (this.isEnabled()) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 點選動作處理 每次點選時將拖動狀態改為 false 並且記錄下點選時的座標 downX downY isDrag=false; downX = event.getX(); // 點選觸屏時的x座標 用於離開螢幕時的x座標作計算 downY = event.getY(); // 點選觸屏時的y座標 用於離開螢幕時的y座標作計算 break; case MotionEvent.ACTION_MOVE: // 滑動動作處理 記錄離開螢幕時的 moveX moveY 用於計算距離 和 判斷滑動事件和點選事件 並作出響應 final float moveX = event.getX() - downX; final float moveY = event.getY() - downY; int l,r,t,b; // 上下左右四點移動後的偏移量 //計算偏移量 設定偏移量 = 3 時 為判斷點選事件和滑動事件的峰值 if (Math.abs(moveX) > 3 ||Math.abs(moveY) > 3) { // 偏移量的絕對值大於 3 為 滑動時間 並根據偏移量計算四點移動後的位置 l = (int) (getLeft() + moveX); r = l+width; t = (int) (getTop() + moveY); b = t+height; //不劃出邊界判斷,最大值為邊界值 // 如果你的需求是可以劃出邊界 此時你要計算可以劃出邊界的偏移量 最大不能超過自身寬度或者是高度 如果超過自身的寬度和高度 view 劃出邊界後 就無法再拖動到介面內了 注意 if(l<0){ // left 小於 0 就是滑出邊界 賦值為 0 ; right 右邊的座標就是自身寬度 如果可以劃出邊界 left right top bottom 最小值的絕對值 不能大於自身的寬高 l=0; r=l+width; }else if(r> maxWidth){ // 判斷 right 並賦值 r= maxWidth; l=r-width; } if(t<0){ // top t=0; b=t+height; }else if(b> maxHeight){ // bottom b= maxHeight; t=b-height; } this.layout(l, t, r, b); // 重置view在layout 中位置 isDrag=true; // 重置 拖動為 true }else { isDrag=false; // 小於峰值3時 為點選事件 } break; case MotionEvent.ACTION_UP: // 不處理 setPressed(false); break; case MotionEvent.ACTION_CANCEL: // 不處理 setPressed(false); break; } return true; } return false; } }
註釋寫的很清晰了 不懂得可以看程式碼 我說一下點選事件的處理 我暴漏了一個方法
public boolean isDrag() {
return isDrag;
}
這個方法返回一個boolean 適用於標識是否滑動 我們利用這個值進行區分點選事件和滑動事件
好 看一下使用
freeview = findViewById(R.id.main_freeview);
freeview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (!freeview.isDrag()) {
Toast.makeText(MainActivity.this, “點選了freeview”, Toast.LENGTH_SHORT).show();
}
}
});
此時只要處理點選事件即可
好了 本文到此完結 如有疑問歡迎私信和留言 原始碼在這裡就不上傳了 有需要可以加我的群 675234574 或加我微信
下面是工具類
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
public class UiUtil {
// 獲取最大寬度
public static int getMaxWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService( Context.WINDOW_SERVICE );
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics( dm );
return dm.widthPixels;
}
// 獲取最大高度
public static int getMaxHeight(Context context) {
WindowManager wm = (WindowManager) context.getSystemService( Context.WINDOW_SERVICE );
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics( dm );
return dm.heightPixels;
}
}