Activity和Window的View的移动的一些思考与体会,腾讯悬浮小火箭的实现策略
事实上写这个也是因为自己实际在项目中用到了才会去研究已经写文章,对于View的移动,其实说实话,已经有很多文章了,既然如此的话,那我实在是不好意思再去重复的讲解,但是和Window的View还是有一些区别的,接下来,我会实际的讲解一下这些区别已经坐标函数的计算方法,当然,最后再讲一下如何实现腾讯的悬浮小火箭,这些都是比较好的干货,我也相信大家都是比较喜欢的,而你在本文中将学会使用View的移动计算坐标,有三个目录
- 1.Activity中View的移动
- 2.Window中View的移动
- 3.实现腾讯悬浮小火箭
我们首先新建一个项目ViewAndWindow来实现三个按钮作为这三个功能的三个Activity跳转,三个Activity分别是
- ActivityActivity
- WindowActivity
- TencentActivity
所以主布局-activity_layout.xml应该是这么写的
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp">
<Button
android:id="@+id/btnActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Activity中View的移动"
android:textAllCaps="false" />
<Button
android:id="@+id/btnWindow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Window中View的移动"
android:textAllCaps="false" />
<Button
android:id="@+id/btnTencent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="腾讯小火箭"
android:textAllCaps="false" />
</LinearLayout>
不可否认,我们的MainActivty只是作为程序的入口,所以他的代码是十分的简单的
package com.lgl.viewandwindow;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btnActivity, btnWindow, btnTencent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
//初始化View
private void initView() {
btnActivity = (Button) findViewById(R.id.btnActivity);
btnActivity.setOnClickListener(this);
btnWindow = (Button) findViewById(R.id.btnWindow);
btnWindow.setOnClickListener(this);
btnTencent = (Button) findViewById(R.id.btnTencent);
btnTencent.setOnClickListener(this);
}
//点击事件
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnActivity:
startActivity(new Intent(this, ActivityActivity.class));
break;
case R.id.btnWindow:
startActivity(new Intent(this, WindowActivity.class));
break;
case R.id.btnTencent:
startActivity(new Intent(this, TencentActivity.class));
break;
}
}
}
而我们的重点也不在这里,而在这些子Activity
一.Activity中View的移动
实际上,View在Activity上移动,还是要依靠事件去传递,总所周知,View的绘制流程一般都是先onMeasure测量,接下来是onLayout确定位置,最后才是onDraw绘制,所以,我们的更新坐标其实是在onLayout进行的,好吧,说这些再多都不如代码来的实际一点,我们在Activity中写一个ImageView
<ImageView
android:id="@+id/ivDraw"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />
我们就是要对他下手了,是的,就在OnTouchListener中进行,OnTouchListener回调中有一个MotionEvent类,他封装了我们触摸事件的大部分动作,其中就包括三大将军
switch (event.getAction()) {
//按下
case MotionEvent.ACTION_DOWN:
break;
//抬起
case MotionEvent.ACTION_UP:
break;
//移动
case MotionEvent.ACTION_MOVE:
break;
而我们如果单单只是移动这个View的话,其实是用不到抬起这个UP的动作的,我们要想实现这个View的移动,首先得知道这个View在哪里,所以我们需要先定义两个变量
//起点坐标
private int startX, startY;
而我们什么时候得到View的初始坐标呢?肯定是在按下这个动作上获取
startX = (int) event.getRawX();
startY = (int) event.getRawY();
而这里,肯定就会有人问,这个getX和getRowX有什么区别,其实区别还是挺大的,前者是获取当前父容器的X坐标,后者是相对于整个屏幕的坐标,OK,获取到之后,我们应该干什么?这个时候我们应该使用到MOVE这个动作了,你在拖动,我计算偏移量并且更新这个View的位置,来达到移动的视觉效果,那我们还得定义几个变量
首先是你的重点坐标,有始有终
//终点坐标
private int endX, endY;
紧接着,会让终点坐标减去起点坐标,来计算这个偏移量,所以有了偏移量的变量
//偏移量
private int dx, dy;
所以,我们MOVE的动作里计算公式应该是这样的
endX = (int) event.getRawX();
endY = (int) event.getRawY();
//计算移动偏移量
dx = endX - startX;
dy = endY - startY;
获取到你移动的偏移量,我们就可以拿到移动后的坐标了,还记得我们在绘制矩形的时候用到的那套公式吗
我们直接套用这套公式,其实就可以得到左上右下的坐标了
int left = tvAddress.getLeft() + dx;
int top = tvAddress.getTop() + dy;
int right = tvAddress.getRight() + dx;
int bottom = tvAddress.getBottom() + dy;
OK,这里,其实有点类似于测量,测量结束之后就可以确定位置了,就得用到我们的onLayout了
//重新部署位置
ivDraw.layout(left, top, right, bottom);
到这里,其实很多人就以为走完了的,其实更新完位置之后,你还要把初始位置给初始化一下,也就是赋值成你更新后的坐标点
//重新初始化坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
对了。记得return true,这里你会问,为什么是true,因为true代表我要消耗掉这个事件,你其他的就不要接收了,你不信的话可以设置一个点击事件看看有没有效果!
这里,我们就算大功告成了,如果你想记录这个坐标点,你就会用到UP了,不多说,我们运行看看效果
但是这里,还需要优化一下,比如,我移动到边上的时候直接就进去了,我们应该放置这个View超过屏幕,对吧,那我们应该怎么做?我们首先先获取到整个屏幕的宽高
wm = (WindowManager) getSystemService(WINDOW_SERVICE);
width = wm.getDefaultDisplay().getWidth();
height = wm.getDefaultDisplay().getHeight();
这样,我们通过WindowManager就能直接拿到宽高了,然后我们在移动的时候可以这样做
//防止上下
if (top < 0 || bottom > height - 20) {
return true;
}
//防止左右
if (left < 0 || right > width) {
return true;
}
这样,我们就可以直接看到效果了
这里的减去20是状态栏的,但是下面的虚拟按键倒是没有考虑进去,不过思路真的可行
好了,上面是步骤,我们就直接把代码全部贴出来吧
package com.lgl.viewandwindow;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
/**
* Created by LGL on 2016/7/28.
*/
public class ActivityActivity extends AppCompatActivity {
private ImageView ivDraw;
//起点坐标
private int startX, startY;
//终点坐标
private int endX, endY;
//偏移量
private int dx, dy;
//窗口管理器
private WindowManager wm;
//屏幕宽高
private int width, height;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_activity);
wm = (WindowManager) getSystemService(WINDOW_SERVICE);
width = wm.getDefaultDisplay().getWidth();
height = wm.getDefaultDisplay().getHeight();
ivDraw = (ImageView) findViewById(R.id.ivDraw);
ivDraw.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
endX = (int) event.getRawX();
endY = (int) event.getRawY();
//计算移动偏移量
dx = endX - startX;
dy = endY - startY;
/**
*根据偏移量更新位置(重新部署位置)
*/
int left = ivDraw.getLeft() + dx;
int top = ivDraw.getTop() + dy;
int right = ivDraw.getRight() + dx;
int bottom = ivDraw.getBottom() + dy;
//防止上下
if (top < 0 || bottom > height - 20) {
return true;
}
//防止左右
if (left < 0 || right > width) {
return true;
}
//重新部署位置
ivDraw.layout(left, top, right, bottom);
//重新初始化坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
}
return true;
}
});
}
}
二.Window中View的移动
Activity上毕竟是有迹可循,那Window上呢?其实窗体上逻辑是差不多的,唯一差的,就是那些函数的调用了,OK,我们进入WindowActivity中,先写个Button启动这个Window
<Button
android:id="@+id/showWindow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Show Window"
android:textAllCaps="false" />
他所对应的点击事件
//点击事件
showWindow = (Button) findViewById(R.id.showWindow);
showWindow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showMoveWindow();
}
});
而我们这个小节的重点就的照顾一下 showMoveWindow()这个方法了,怎么实现一个Window不是今天的重点,而且也确实没什么可讲的,我就直接上代码了
//窗口管理器
wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
//布局参数
final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
//WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | 不能触摸
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
//格式
layoutParams.format = PixelFormat.TRANSLUCENT;
//类型
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
ivDraw = new ImageView(this);
ivDraw.setBackgroundResource(R.mipmap.ic_launcher);
//加载view
wm.addView(ivDraw, layoutParams);
这段代码就能实现一个window了,我们可以看一下
我们需要权限哦
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
我们是直接new了一个ImageView的,但是不妨碍我们使用View的移动,我们直接实现它的触摸事件
//触摸事件
ivDraw.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
});
我这里暂时也只是实现了两个动作,因为作为演示我们的UP确实用不上,我们有了前车之鉴,我们直接定义我们需要的变量吧
//起始坐标
private int startX, startY;
//终点坐标
private int endX, endY;
//偏移量
private int dx, dy;
OK,老套路,在DOWN中,我们只是获取当前的坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
但是移动的时候,获取的就不是左上右下了,而是他的x和y坐标,因为他是window,所以我们用到的是LayoutParams,更新位置也是使用的LayoutParams,他有一个updateViewLayout的方法
endX = (int) event.getRawX();
endY = (int) event.getRawY();
//计算移动偏移量
dx = endX - startX;
dy = endY - startY;
/**
*根据偏移量更新位置(重新部署位置)
*/
layoutParams.x += dx;
layoutParams.y += dy;
//更新位置
wm.updateViewLayout(ivDraw, layoutParams);
//重新初始化坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
当然,最后return true;我们试试
而因为他是window的改哪里,他并不需要去做一些超出边距的处理,很nice
把这部分代码也全部贴上来
package com.lgl.viewandwindow;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
/**
* Created by LGL on 2016/7/28.
*/
public class WindowActivity extends AppCompatActivity {
private Button showWindow;
//窗口管理器
private WindowManager wm;
//图片
private ImageView ivDraw;
//起始坐标
private int startX, startY;
//终点坐标
private int endX, endY;
//偏移量
private int dx, dy;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_window);
//点击事件
showWindow = (Button) findViewById(R.id.showWindow);
showWindow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showMoveWindow();
}
});
}
//显示窗口
private void showMoveWindow() {
//窗口管理器
wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
//布局参数
final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
//WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | 不能触摸
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
//格式
layoutParams.format = PixelFormat.TRANSLUCENT;
//类型
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
ivDraw = new ImageView(this);
ivDraw.setBackgroundResource(R.mipmap.ic_launcher);
//触摸事件
ivDraw.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
endX = (int) event.getRawX();
endY = (int) event.getRawY();
//计算移动偏移量
dx = endX - startX;
dy = endY - startY;
/**
*根据偏移量更新位置(重新部署位置)
*/
layoutParams.x += dx;
layoutParams.y += dy;
//更新位置
wm.updateViewLayout(ivDraw, layoutParams);
//重新初始化坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
}
return true;
}
});
//加载view
wm.addView(ivDraw, layoutParams);
}
}
三.实现腾讯悬浮小火箭
到这里,其实已经算是知道点逻辑了,我们就是用window去做的一个操作,既然如此,那我们就直接基于上面的代码去去实现这个小火箭吧,还是原来的代码,只是把图片更换成了一个小火箭,然后为了使它是是一个动态的效果,我们可以给他设置一个切换的动画
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:drawable="@drawable/desktop_rocket_launch_in"
android:duration="200"/>
<item
android:drawable="@drawable/desktop_rocket_launch_out"
android:duration="200"/>
</animation-list>
这个其实就是两张图片的切换效果,我们直接去开启他
mView = View.inflate(getApplicationContext(), R.layout.rocket_window, null);
ivRocket = (ImageView) mView.findViewById(R.id.ivRocket);
AnimationDrawable anim = (AnimationDrawable) ivRocket.getBackground();
anim.start();
OK,现在我们去计算他的起飞了,而且还要考虑到他背景,我们其实可以大胆的使用一个Activity去做,我们在UP这个动作结束的时候就去计算坐标,当满足某一个坐标范围的时候就去启动动画和启动背景动画,那我们应该这样计算
case MotionEvent.ACTION_UP:
Log.i(TAG,"抬起");
Log.i(TAG,"抬起坐标:" + startX + ":" + startY);
Log.i(TAG,"条件 : 200 < x >" + (width - 100) + "并且 y > " + (height - 200));
//设置大致的发射范围
if (layoutParams.x > 50 && layoutParams.x < 250 && layoutParams.y > 350) {
//发射火箭
sendRocket();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//启动动画
Intent i = new Intent(getApplicationContext(), BackgroundActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
}
},1000);
}
break;
这里其实偷了个懒,没有去做屏幕的严格适配,有兴趣的伙伴可以参考一下
可以看到我们有一个 sendRocket();方法就是启动小火箭,启动背景就是启动这个BackgroundActivity,我们先看小火箭
/**
* 发射火箭的方法
*/
private void sendRocket() {
new Thread(new Runnable() {
@Override
public void run() {
//动画 y坐标一直减少,实现上升动画
for (int i = 0; i <= height / 50; i++) {
//每循环一次减去乘以5
int y = height - i * 100;
Log.i(TAG,"y = " + y);
Message msg = new Message();
msg.arg1 = y;
handler.sendMessage(msg);
//暂停一下
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
因为要有节奏感,所以开了个handler来延迟一下,但是子线程不能更新主UI的,所以我们需要发一个handler来更新坐标
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
layoutParams.y = msg.arg1;
//更新窗口
wm.updateViewLayout(mView, layoutParams);
}
};
最后就是这个Activity了,里面真的啥也没有
package com.lgl.viewandwindow;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.animation.AlphaAnimation;
import android.widget.ImageView;
/**
* 烟雾动画
* Created by LGL on 2016/7/30.
*/
public class BackgroundActivity extends Activity {
private ImageView smoke_m, smoke_t;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_background);
initView();
}
//初始化
private void initView() {
smoke_m = (ImageView) findViewById(R.id.smoke_m);
smoke_t = (ImageView) findViewById(R.id.smoke_t);
//渐变动画
AlphaAnimation alpha = new AlphaAnimation(0, 1);
alpha.setDuration(2000);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
},4000);
}
}
要注意的一点就是他党主题需要透明下
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
看我们最终的效果
当然,我这做的是比较挫的,我想表达的其实就只是一种编码的思路罢了,掌握了思想,怎么去优化,那都是比较简单的事情了,好的,最后把代码发上来,当然,也提供了Demo下载的
package com.lgl.viewandwindow;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.drawable.AnimationDrawable;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
/**
* 小火箭
* Created by LGL on 2016/7/28.
*/
public class RocketService extends Service {
public static final String TAG = RocketService.class.getSimpleName();
//窗口管理器
private WindowManager wm;
//图片
private View mView;
//起始坐标
private int startX, startY;
//终点坐标
private int endX, endY;
//偏移量
private int dx, dy;
//小火箭
private ImageView ivRocket;
private WindowManager.LayoutParams layoutParams;
//屏幕宽高
private int width, height;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
layoutParams.y = msg.arg1;
//更新窗口
wm.updateViewLayout(mView, layoutParams);
}
};
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
getWindowBig();
showRocket();
}
//显示小火箭
private void showRocket() {
//窗口管理器
wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
//布局参数
layoutParams = new WindowManager.LayoutParams();
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
//WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | 不能触摸
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
//格式
layoutParams.format = PixelFormat.TRANSLUCENT;
//类型
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
mView = View.inflate(getApplicationContext(), R.layout.rocket_window, null);
ivRocket = (ImageView) mView.findViewById(R.id.ivRocket);
AnimationDrawable anim = (AnimationDrawable) ivRocket.getBackground();
anim.start();
//触摸事件
mView.setOnTouchListener(
new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG,"按下");
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
//L.i("移动");
endX = (int) event.getRawX();
endY = (int) event.getRawY();
//计算移动偏移量
dx = endX - startX;
dy = endY - startY;
/**
*根据偏移量更新位置(重新部署位置)
*/
layoutParams.x += dx;
layoutParams.y += dy;
//更新位置
wm.updateViewLayout(mView, layoutParams);
//重新初始化坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_UP:
Log.i(TAG,"抬起");
Log.i(TAG,"抬起坐标:" + startX + ":" + startY);
Log.i(TAG,"条件 : 200 < x >" + (width - 100) + "并且 y > " + (height - 200));
//设置大致的发射范围
if (layoutParams.x > 50 && layoutParams.x < 250 && layoutParams.y > 350) {
//发射火箭
sendRocket();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//启动动画
Intent i = new Intent(getApplicationContext(), BackgroundActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
}
},1000);
}
break;
}
return true;
}
}
);
//加载view
wm.addView(mView, layoutParams);
}
/**
* 发射火箭的方法
*/
private void sendRocket() {
new Thread(new Runnable() {
@Override
public void run() {
//动画 y坐标一直减少,实现上升动画
for (int i = 0; i <= height / 50; i++) {
//每循环一次减去乘以5
int y = height - i * 100;
Log.i(TAG,"y = " + y);
Message msg = new Message();
msg.arg1 = y;
handler.sendMessage(msg);
//暂停一下
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
wm.removeView(mView);
}
/**
* 获取屏幕的宽高
*/
private void getWindowBig() {
wm = (WindowManager) getSystemService(WINDOW_SERVICE);
width = wm.getDefaultDisplay().getWidth();
height = wm.getDefaultDisplay().getHeight();
Log.i(TAG,"屏幕的宽高" + width + ":" + height);
}
}
今天的博客就先到这里,谢谢大家,有兴趣的可以加群