一种是弹窗的拖动布局,一种是非弹窗。
代码如下:
非弹窗:这里加载了一个本地的视频
import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; import android.widget.MediaController; import android.widget.VideoView; import androidx.appcompat.app.AppCompatActivity; public class MoveActivity extends AppCompatActivity implements View.OnTouchListener{ private VideoView video; private int sx; private int sy; private SharedPreferences sp; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_move); if (getSupportActionBar() != null){ getSupportActionBar().hide(); } sp = this.getSharedPreferences("config", Context.MODE_PRIVATE); video = this.findViewById(R.id.video_view); video.setVideoPath("android.resource://" + this.getPackageName() + "/" + R.raw.videos); video.setOnTouchListener(this); //创建MediaController对象 MediaController mediaController = new MediaController(this); video.setMediaController(mediaController); //让videoView 和 MediaController相关联 video.setFocusable(true); //让VideoView获得焦点 video.start(); //开始播放视频 } @Override protected void onResume() { super.onResume(); } @Override public boolean onTouch(View v, MotionEvent event) { switch (v.getId()) { // 如果手指放在imageView上拖动 case R.id.video_view: // event.getRawX(); //获取手指第一次接触屏幕在x方向的坐标 switch (event.getAction()) { case MotionEvent.ACTION_DOWN:// 获取手指第一次接触屏幕 sx = (int) event.getRawX(); sy = (int) event.getRawY(); break; case MotionEvent.ACTION_MOVE:// 手指在屏幕上移动对应的事件 int x = (int) event.getRawX(); int y = (int) event.getRawY(); // 获取手指移动的距离 int dx = x - sx; int dy = y - sy; // 得到imageView最开始的各顶点的坐标 int l = video.getLeft(); int r = video.getRight(); int t = video.getTop(); int b = video.getBottom(); // 更改imageView在窗体的位置 video.layout(l + dx, t + dy, r + dx, b + dy); // 获取移动后的位置 sx = (int) event.getRawX(); sy = (int) event.getRawY(); break; case MotionEvent.ACTION_UP:// 手指离开屏幕对应事件 // 记录最后图片在窗体的位置 int lasty = video.getTop(); int lastx = video.getLeft(); SharedPreferences.Editor editor = sp.edit(); editor.putInt("lasty", lasty); editor.putInt("lastx", lastx); editor.commit(); break; } break; } return true;// 不会中断触摸事件的返回 } }
布局:
弹窗试:弹窗加载一个视频
import androidx.appcompat.app.AppCompatActivity; import android.Manifest; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.KeyEvent; import android.view.Menu; import android.view.View; import com.example.mymovevideo.databinding.ActivityMainBinding; import com.example.mymovevideo.tools.HomeWatcher; import com.example.mymovevideo.tools.SmallWindowsView; import pub.devrel.easypermissions.EasyPermissions; public class MainActivity extends AppCompatActivity { SmallWindowsView smallWindowsView; ActivityMainBinding mainBinding; private HomeWatcher mHomeWatcher; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mainBinding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(mainBinding.getRoot()); smallWindowsView = new SmallWindowsView(MainActivity.this); mainBinding.btShowWindow.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (Build.VERSION.SDK_INT >= 23) { if (!(Settings.canDrawOverlays(MainActivity.this))) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); return; } } else { if (!EasyPermissions.hasPermissions(MainActivity.this, Manifest.permission.SYSTEM_ALERT_WINDOW)) { EasyPermissions.requestPermissions(MainActivity.this, "需要权限用以展示悬浮窗", 2048, Manifest.permission.SYSTEM_ALERT_WINDOW); return; } } smallWindowsView.show(); mainBinding.btShowWindow.setClickable(false); } }); mHomeWatcher = new HomeWatcher(this); mHomeWatcher.setOnHomePressedListener(new HomeWatcher.OnHomePressedListener() { @Override public void onHomePressed() { smallWindowsView.dismiss(); mainBinding.btShowWindow.setClickable(true); } @Override public void onHomeLongPressed() { smallWindowsView.dismiss(); mainBinding.btShowWindow.setClickable(true); } }); mHomeWatcher.startWatch(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { //按下BACK键 if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { smallWindowsView.dismiss(); mainBinding.btShowWindow.setClickable(true); } return super.onKeyDown(keyCode, event); } @Override public void onOptionsMenuClosed(Menu menu) { super.onOptionsMenuClosed(menu); smallWindowsView.dismiss(); mainBinding.btShowWindow.setClickable(true); } }
布局:
工具类:
import android.content.Context; import android.util.AttributeSet; import android.widget.VideoView; public class ConditionVideoView extends VideoView { public ConditionVideoView(Context context) { super(context); } public ConditionVideoView(Context context, AttributeSet attrs) { super(context, attrs); } public ConditionVideoView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //使视频全屏播放 int width = getDefaultSize(0, widthMeasureSpec); int height = getDefaultSize(0, heightMeasureSpec); setMeasuredDimension(width, height); } }
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; public class HomeWatcher { private Context mContext; private IntentFilter mFilter; private OnHomePressedListener mListener; private InnerRecevier mRecevier; // 回调接口 public interface OnHomePressedListener { public void onHomePressed(); public void onHomeLongPressed(); } public HomeWatcher(Context context) { mContext = context; mRecevier = new InnerRecevier(); mFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); } /** * 设置监听 * * @param listener */ public void setOnHomePressedListener(OnHomePressedListener listener) { mListener = listener; } /** * 开始监听,注册广播 */ public void startWatch() { if (mRecevier != null) { mContext.registerReceiver(mRecevier, mFilter); } } /** * 广播接收者 */ class InnerRecevier extends BroadcastReceiver { final String SYSTEM_DIALOG_REASON_KEY = "reason"; final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps"; final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey"; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) { String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY); if (reason != null) { if (mListener != null) { if (reason.equals(SYSTEM_DIALOG_REASON_HOME_KEY)) { // 短按home键 mListener.onHomePressed(); } else if (reason.equals(SYSTEM_DIALOG_REASON_RECENT_APPS)) { // 长按home键 mListener.onHomeLongPressed(); } } } } } } }
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; public class HomeWatcherReceiver extends BroadcastReceiver { private static final String LOG_TAG = "HomeReceiver"; private static final String SYSTEM_DIALOG_REASON_KEY = "reason"; private static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps"; private static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey"; private static final String SYSTEM_DIALOG_REASON_LOCK = "lock"; private static final String SYSTEM_DIALOG_REASON_ASSIST = "assist"; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) { String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY); Log.i(LOG_TAG, "reason: " + reason); if (SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason)) { // 短按Home键 Log.i(LOG_TAG, "homekey"); } else if (SYSTEM_DIALOG_REASON_RECENT_APPS.equals(reason)) { // 长按Home键 或者 activity切换键 Log.i(LOG_TAG, "long press home key or activity switch"); } else if (SYSTEM_DIALOG_REASON_LOCK.equals(reason)) { // 锁屏 Log.i(LOG_TAG, "lock"); } else if (SYSTEM_DIALOG_REASON_ASSIST.equals(reason)) { // samsung 长按Home键 Log.i(LOG_TAG, "assist"); } } } }
import android.content.Context; import android.graphics.PixelFormat; import android.os.Build; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.MediaController; import android.widget.VideoView; import androidx.annotation.NonNull; import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.core.content.ContextCompat; import com.example.mymovevideo.R; import com.google.common.util.concurrent.ListenableFuture; import java.util.concurrent.ExecutionException; public class SmallWindowsView extends FrameLayout { private int mSlop;//触发移动事件的最小距离 private float downX;//手指放下去的x坐标 private float downY;//手指放下去的Y坐标 /** * 下面四个数据都为像素 */ private int screenWidth;//屏幕宽度 private int screenHeight;//屏幕高度 private int viewWidth;//小窗的宽度 private int viewHeight;//小窗的高度 private WindowManager wm;//窗口管理器,用来把view添加进窗口层 private WindowManager.LayoutParams wmParams; private ProcessCameraProvider mCameraProvider; private ListenableFuturemCameraProviderFuture; public SmallWindowsView(@NonNull Context context) { super(context); init(context); } private void init(Context context) { ViewConfiguration vc = ViewConfiguration.get(getContext()); // 窗口里的布局 View view = View.inflate(context, R.layout.view_floating_window, null); mSlop = vc.getScaledTouchSlop(); screenWidth = getContext().getResources().getDisplayMetrics().widthPixels; screenHeight = getContext().getResources().getDisplayMetrics().heightPixels; viewWidth = dp2px(getContext(), 130); viewHeight = dp2px(getContext(), 130); mCameraProviderFuture = ProcessCameraProvider.getInstance(context); mCameraProviderFuture.addListener(() -> { try { mCameraProvider = mCameraProviderFuture.get(); } catch (ExecutionException | InterruptedException e) { // 这里不用处理 } }, ContextCompat.getMainExecutor(context)); VideoView video = view.findViewById(R.id.videoView); video.setVideoPath("android.resource://" + context.getPackageName() + "/" + R.raw.videos); /** * 控制视频的播放 主要通过MediaController控制视频的播放 */ //创建MediaController对象 MediaController mediaController = new MediaController(context); video.setMediaController(mediaController); //让videoView 和 MediaController相关联 video.setFocusable(true); //让VideoView获得焦点 video.start(); //开始播放视频 // 实际上就是拿到一个View从WindowManager给addView进去 LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); addView(view, params); } //dp转px public int dp2px(Context context, int dp) { return (int) (getDensity(context) * dp + 0.5); } public float getDensity(Context context) { return context.getResources().getDisplayMetrics().density; } public void show() { wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); wmParams = new WindowManager.LayoutParams( viewWidth, viewHeight, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,//8.0以上需要用这个权限 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, PixelFormat.TRANSLUCENT); wmParams.gravity = Gravity.NO_GRAVITY; wmParams.x = screenWidth / 2 - viewWidth / 2; wmParams.y = screenHeight / 2 - viewHeight / 2; // 适配8.0以上 if (Build.VERSION.SDK_INT > 24) { wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; } wm.addView(this, wmParams); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } //拦截触摸事件自己消费 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true; } private long downTime; private float lastMoveX; private float lastMoveY; //消费触摸事件 @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = event.getRawX(); downY = event.getRawY(); lastMoveX = downX; lastMoveY = downY; downTime = System.currentTimeMillis(); break; case MotionEvent.ACTION_MOVE: float moveX = event.getRawX(); float moveY = event.getRawY(); //就两个坐标算他们距离要大于触发移动事件的最小距离 //这里也可以减去lastMoveX lastMoveY 但是移动会有卡顿感 因此这里使用的还是downX downY if (Math.pow(Math.abs(moveX - downX), 2) + Math.pow(Math.abs(moveY - downY), 2) > Math.pow(mSlop, 2)) { updateViewPosition(moveX - lastMoveX, moveY - lastMoveY); lastMoveX = moveX; lastMoveY = moveY; } break; case MotionEvent.ACTION_UP: float upX = event.getRawX(); float upY = event.getRawY(); long upTime = System.currentTimeMillis(); long time = upTime - downTime; //点击事件实现 点击小窗口消失 //这里加了时间判断,是因为假如移动到原来的地方,也会触发成点击事件 if (Math.pow(Math.abs(upX - downX), 2) + Math.pow(Math.abs(upY - downY), 2) < Math.pow(mSlop, 2) && time < 1000) { showRtcVideo(); } break; } return true; } private void showRtcVideo() { dismiss(); } public void dismiss() { if (null != wm) { wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); wm.removeView(this); } } private void updateViewPosition(float moveX, float moveY) { wmParams.gravity = Gravity.NO_GRAVITY; //更新浮动窗口位置参数 wmParams.x = (int) (wmParams.x + moveX); wmParams.y = (int) (wmParams.y + moveY); //刷新显示 wm.updateViewLayout(this, wmParams); } }
工具类布局:
配置文件:
plugins { id 'com.android.application' } android { namespace 'com.example.mymovevideo' compileSdk 32 defaultConfig { applicationId "com.example.mymovevideo" minSdk 21 targetSdk 32 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } dataBinding { enabled = true } viewBinding { enabled = true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'pub.devrel:easypermissions:3.0.0' implementation "androidx.camera:camera-core:1.1.0-alpha11" implementation "androidx.camera:camera-camera2:1.1.0-alpha11" implementation "androidx.camera:camera-lifecycle:1.1.0-alpha11" implementation "androidx.camera:camera-view:1.0.0-alpha31" implementation "androidx.camera:camera-extensions:1.0.0-alpha31" implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' }
清单文件