刚去公司实习,需要做几个小项目,这个是其中一个
需求是这样子:
1.打应界面后扫描出正在运行的应用程序
2.以随机的方式让这些正在运行的应用的图标在屏幕内作移动与旋转运动, 当图标相互碰撞或碰到屏幕边界时作相应的反弹移动效果.
3.开始时Boom Boom Boost 应用的启动图标位于屏幕中心, 用户按下图标拖动调整方向及力度线, 松开时以确定的方向及力度开始移动同时旋转, 运动效果同2)中的正在运行应用的图标
4.当Boom Boom Boost 应用的图标碰撞到其他应用图标时, 则查杀结速图标对应的应用程序, 同时其图标渐出消失.
5.当Boom Boom Boost 应用的图标运动时, 用户按下时可将其停止重新调整运动方向及速度, 操作同3)中的描述
我们看一下最终实现的效果图:
该项目不用到第三方库,使用原生api进行实现
首先肯定是获取正在运行的应用了。那么我们先看一下项目的核心服务类CleanService。该类主要TaskScan和TaskClean组成。我们先看看TaskScan.主要是对”proc”文件夹进行扫描,然后对获取的进程进行排序并且去重,就可以获得当前正在运行的应用程序列表了。
private class TaskScan extends AsyncTask<Void,Integer,List<AppProcessInfo>>{
private int mAppCount = 0;
@Override
protected void onPreExecute() {
if (mOnActionListener != null) {
mOnActionListener.onScanStarted(CleanService.this);
}
}
@Override
protected List doInBackground(Void... voids) {
list = new ArrayList<>();
ApplicationInfo appInfo = null;
AppProcessInfo abAppProcessInfo = null;
List appProcessList = ProcessManager.getRunningAppProcesses();
publishProgress(0,appProcessList.size());
for (AndroidAppProcess appProcessInfo : appProcessList){
publishProgress(++mAppCount, appProcessList.size());
abAppProcessInfo = new AppProcessInfo(
appProcessInfo.getPackageName(), appProcessInfo.pid,
appProcessInfo.uid);
try {
appInfo = packageManager.getApplicationInfo(appProcessInfo.getPackageName(), 0);
if ((appInfo.flags&ApplicationInfo.FLAG_SYSTEM)!=0){
abAppProcessInfo.setSystem(true);
}else {
abAppProcessInfo.setSystem(false);
}
Drawable icon = appInfo.loadIcon(packageManager)==null?
getResources().getDrawable(R.mipmap.ic_launcher):appInfo.loadIcon(packageManager);
String name = appInfo.loadLabel(packageManager).toString();
abAppProcessInfo.setIcon(icon);
abAppProcessInfo.setAppName(name);
}catch (PackageManager.NameNotFoundException e){
//服务的命名
if (appProcessInfo.getPackageName().indexOf(":") != -1){
appInfo = getApplicationInfo(appProcessInfo.getPackageName().split(":")[0]);
if (appInfo!=null){
Drawable icon = appInfo.loadIcon(packageManager);
abAppProcessInfo.setIcon(icon);
}else {
abAppProcessInfo.setIcon(mContext.getResources().getDrawable(R.mipmap.ic_launcher));
}
}
abAppProcessInfo.setSystem(true);
abAppProcessInfo.setAppName(appProcessInfo.getPackageName());
}
long memsize = activityManager.getProcessMemoryInfo(new int[]{appProcessInfo.pid})[0].getTotalPrivateDirty() * 1024;
abAppProcessInfo.setMemory(memsize);
if (!abAppProcessInfo.isSystem())
list.add(abAppProcessInfo);
}
//APP去重
ComparatorApp comparator = new ComparatorApp();
Collections.sort(list,comparator);
int lastUid = 0;
int index = -1;
List newList = new ArrayList<>();
for (AppProcessInfo info:list){
if (lastUid == info.getUid()){
AppProcessInfo nowInfo = newList.get(index);
newList.get(index).setMemory(nowInfo.getMemory()+info.getMemory());
}else {
index++;
newList.add(info);
lastUid = info.getUid();
}
}
return newList;
}
@Override
protected void onProgressUpdate(Integer... values) {
if (mOnActionListener != null){
mOnActionListener.onScanProgressUpdated(CleanService.this,values[0],values[1]);
}
}
@Override
protected void onPostExecute(List appProcessInfos) {
if (mOnActionListener != null){
mOnActionListener.onScanCompleted(CleanService.this,appProcessInfos);
}
mIsScanning = false;
}
}
一般我们都用activityManager.getRunningAppProcesses();
进行获取。不过在安卓5.0之后获取不到数据,只能获取自身运行的进程
后来上网查找资料,通过扫描”proc”文件夹进行获取,关键代码如下,不过这个方法仍然存在一定的兼容性问题。在魅族、联想、自带的模拟器都没问题,但是在国产的荣耀上却扫描不到,国产手机修改系统已经不是一天两天的事了。。。。如果有其它好的办法,麻烦告知一下。
public static List getRunningAppProcesses() {
List processes = new ArrayList<>();
File[] files = new File("/proc").listFiles();
for (File file : files) {
if (file.isDirectory()) {
int pid;
try {
pid = Integer.parseInt(file.getName());
} catch (NumberFormatException e) {
continue;
}
try {
processes.add(new AndroidAppProcess(pid));
} catch (AndroidAppProcess.NotAndroidAppProcessException ignored) {
} catch (IOException e) {
// If you are running this from a third-party app, then system apps will not be
// readable on Android 5.0+ if SELinux is enforcing. You will need root access or an
// elevated SELinux context to read all files under /proc.
// See: https://su.chainfire.eu/#selinux
}
}
}
return processes;
}
扫描之后,肯定是杀死进程了,那么如何杀呢?
网上有这样一种方法:
Method forceStopPackage = activityManager.getClass()
.getDeclaredMethod("forceStopPackage", String.class);
forceStopPackage.setAccessible(true);
forceStopPackage.invoke(activityManager, packageName);
经过测试,此方法不靠谱,这个需要系统权限,即使在mainifests中讲应用使用系统共享的uid,也是使用不了系统的权限。这个作为系统的保留api,只给系统级应用使用。
靠谱的还是这个办法,通过包名进行查杀:
activityManager.killBackgroundProcesses(packageName);
所以,在这给出TaskClean的核心代码,基本思路是获取当前的内存大小,然后根据包名,对应用进行查杀。查杀结束之后,再获取当前内存大小,与之前做对比,得出清理内存的大小。
public class TaskClean extends AsyncTask<String,Void,Long>{
@Override
protected Long doInBackground(String... processName) {
long beforeMemory = 0;
long endMemory = 0;
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
beforeMemory = memoryInfo.availMem;
List appProcessList = ProcessManager.getRunningAppProcesses();
for (AndroidAppProcess info : appProcessList) {
if (info.getPackageName().equals(processName[0]))
killBackgroundProcesses(processName[0]);
}
activityManager.getMemoryInfo(memoryInfo);
endMemory = memoryInfo.availMem;
return endMemory - beforeMemory;
}
@Override
protected void onPreExecute() {
if (mOnActionListener != null){
mOnActionListener.onCleanStarted(CleanService.this);
}
}
@Override
protected void onPostExecute(Long result) {
if (mOnActionListener != null) {
mOnActionListener.onCleanCompleted(CleanService.this, result);
}
}
}
public long getAvailMemory(Context context) {
// 获取android当前可用内存大小
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
// 当前系统可用内存 ,将获得的内存大小规格化
return memoryInfo.availMem;
}
完成,那么接下去就是界面的显示了。我们可以获得运行的应用的信息。
首先写一个自定义view,用来显示获取的app。主要就是创建一个工厂类,初始化各种参数,如速度,角度,正向旋转或者逆向旋转。通过degree,变化x和y坐标。通过角度向量进行分解,x的变化值为cos(degree)。。。我们创建一个AppIcon的bean类,用于运行程序的轨迹描述。下面是AppIconView的边界检测和边界碰撞反弹的核心代码。
changePoint(){
x += speed * Math.cos(degree);
y += speed * Math.sin(degree);
if (y >= App.sScreenHeight - height || y <= 0)
degree = -degree;
if (x >= App.sScreenWidth - width || x <= 0)
degree = (int) (Math.PI - degree);
if (x < 0) {
x = 0;
}
if (x > App.sScreenWidth - width ) {
x = (int) (App.sScreenWidth - width);
}
if (y < 0) {
y = 0;
}
if (y > App.sScreenHeight - height ) {
y = App.sScreenHeight - height ;
}
}
有了上述的bean类之后,我们可以自定义一个AppIconView,用于显示运行程序的view。那么,首先,我们可以创建一个工厂类,用于初始化AppIcon的各种参数。
//工厂类,创建一大波的APP
private class IconFactory {
private List mList = new ArrayList<>();
private Random random = new Random();
public IconFactory(List list){
this.mList = list;
}
public AppIcon generateAppIcon(AppProcessInfo info,int i){
AppIcon appIcon = new AppIcon();
appIcon.setRotateAngle(random.nextInt(360));
appIcon.setRotateDirection(random.nextInt(2));
appIcon.setDegree((int)(Math.random()*Math.PI*2));
appIcon.setSpeed(10);
appIcon.setX(App.sScreenWidth/2);
appIcon.setY(App.sScreenHeight/2);
appIcon.setStartTime(System.currentTimeMillis()+i*500);
appIcon.setInfo(info);
appIcon.setKilled(false);
return appIcon;
}
public List generateAppIcons(){
return generateAppIcons(mList.size());
}
private List generateAppIcons(int appSize){
List apps = new LinkedList<>();
for (int i = 0 ;i < appSize;i++){
apps.add(generateAppIcon(mList.get(i),i));
}
return apps;
}
}
有了这些参数,我们就可以画出自定义view了。来到draw类,代码如下。这里要考虑的是,自定义view需要边移动边旋转,所以使用Matrix可以实现,对于程序被杀死之后渐变消失,我们可以使用bitmaputil修改bitmap的透明度,达到渐变的效果。如果程序被杀死的事件+渐变消失事件>当前时间,那么我们不画出当前的appicon,这样的话也省了一部分内存。
private void drawAppIcon(Canvas canvas){
long currentTime = System.currentTimeMillis();
for(int i = 0 ; i < mList.size();i++){
AppIcon appIcon = mList.get(i);
if (currentTime > appIcon.getStartTime()&&appIcon.getStartTime()!=0){
getAppIconLocation(appIcon);
canvas.save();
Matrix matrix = new Matrix();
float transX = appIcon.getX();
float transY = appIcon.getY();
matrix.postTranslate(transX,transY);
float rotateFraction = ((currentTime-appIcon.getStartTime())%APP_FLOAT_TIME)/(float)APP_FLOAT_TIME;
int angle = (int)(rotateFraction * 360);
int rotate = appIcon.getRotateDirection() == 0? angle + appIcon.getRotateAngle(): -angle+appIcon.getRotateAngle();
matrix.postRotate(rotate,transX + mAppWidth[i]/2 ,transY +mAppHeight[i]/2);
if (!appIcon.isKilled())
canvas.drawBitmap(mAppBitmap[i],matrix,mBitmapPaint);
else {
long time = currentTime - appIcon.getKilledTime();
if (timeint percent = (int) (time * 100 / FADE_OUT_TIME) > 100 ? 0 : 100-(int) (time * 100 / FADE_OUT_TIME);
canvas.drawBitmap(BitmapUtil.adjustOpacity(mAppBitmap[i], percent), matrix, mBitmapPaint);
}
}
canvas.restore();
}
}
checkCollision();
}
接下来就是主view的实现,也是一个自定义view,初始化在中间,然后按住view的时候,可以拖拽以及显示一个箭头。释放的时候根据拉拽的距离给予不同的速度。其中主要就是箭头的绘制以及释放角度degree的确定。
我们先来看下BoomView的ontouch事件。
1.按下手指的时候,我们判断是否在boomview的边界范围内,如果是的话,继续执行。否则,移动事件不生效。
2.移动的时候,进行边界检测,如果不超过边界,则根据移动距离移动。
3.抬起手指的时候,记录当前的坐标,和按下手指的坐标进行计算出当前的偏移角度和距离,由此来确定速度大小和移动方向。
@Override
public boolean onTouch(View view, MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (TouchUtil.isTouchView(x,y,App.sScreenWidth/2-mWidth+dx,App.sScreenHeight/2-mHeight+dy,mWidth,mHeight)) {
isMove = true;
isStrat = true;
isTouchView = true;
pointDownX = x;
pointDownY = y;
speed = 0;
}else {
isTouchView = false;
}
break;
case MotionEvent.ACTION_MOVE:
if (isTouchView) {
int dx = mLastX - x;
int dy = mLastY - y;
if (App.sScreenWidth / 2 -mWidth + this.dx - dx < 0)
dx = 0;
if (App.sScreenWidth / 2 + this.dx - dx > App.sScreenWidth )
dx = 0;
if (App.sScreenHeight / 2 -mHeight + this.dy - dy < 0)
dy = 0;
if (App.sScreenHeight / 2 + this.dy - dy > App.sScreenHeight )
dy = 0;
changePoint(dx, dy);
}
break;
case MotionEvent.ACTION_UP:
if (isTouchView) {
isMove = false;
speed = Math.sqrt(
(pointDownX - mLastX) * (pointDownX - mLastX)
+ (pointDownY - mLastY) * (pointDownY - mLastY)
) / 20;
startTime = System.currentTimeMillis();
degree = AngelUtil.CalulateXYAnagle(pointDownX, pointDownY, x, y) / 180 * Math.PI;
Moving();
}
break;
}
mLastX = x;
mLastY = y;
return true;
}
释放手指之后,BoomView会进行移动,接下来我们来看一下BoomView的moving()代码。根据角度进行偏移,边界碰撞之后调整角度。
private void Moving() {
double dx = -speed * Math.cos(degree);
double dy = speed * Math.sin(degree);
if (App.sScreenWidth/2-mWidth+this.dx-dx<0) {
dx = 0;
degree = Math.PI - degree;
}
if (App.sScreenWidth/2+this.dx-dx>App.sScreenWidth) {
dx = 0;
degree = Math.PI - degree;
}
if (App.sScreenHeight/2-mHeight+this.dy-dy<0) {
dy = 0;
degree = -degree;
}
if (App.sScreenHeight/2+this.dy-dy>App.sScreenHeight){
dy = 0;
degree = - degree;
}
changePoint(dx,dy);
}
到这里,BoomView的移动轨迹已经确定了,接下来,就是把视图画出来。首先,判断BoomView是否在移动,如果在移动,则根据按下的点和当前BoomView的坐标画出箭头和直线。并根据当前时间对BoomView进行旋转。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
//画boom图像
Matrix matrix = new Matrix();
matrix.postTranslate(App.sScreenWidth/2-mWidth+dx,App.sScreenHeight/2-mHeight+dy);
long currentTime = System.currentTimeMillis();
float transX = App.sScreenWidth/2-mWidth/2+dx;
float transY = App.sScreenHeight/2-mHeight/2+dy;
if (!isMove) {
matrix.postRotate((currentTime - startTime) % APP_FLOAT_TIME / (float) APP_FLOAT_TIME * 360, transX, transY);
}
canvas.drawBitmap(mBitmap,matrix,mBitmapPaint);
//画箭头
if (isMove&&isStrat){
//先画直线
int mLastX = App.sScreenWidth/2-mWidth/2+dx;
int mLastY = App.sScreenHeight/2-mHeight/2+dy;
double dx1 = (mLastX-pointDownX)*Math.cos(ARROW_ANGLE1) -
(mLastY-pointDownY)*Math.sin(ARROW_ANGLE1) ;
double dy1 = (mLastY-pointDownY)*Math.cos(ARROW_ANGLE1) +
(mLastX-pointDownX)*Math.sin(ARROW_ANGLE1);
double dx2 = (mLastX-pointDownX)*Math.cos(ARROW_ANGLE2) -
(mLastY-pointDownY)*Math.sin(ARROW_ANGLE2) ;
double dy2 = (mLastY-pointDownY)*Math.cos(ARROW_ANGLE2) +
(mLastX-pointDownX)*Math.sin(ARROW_ANGLE2);
double hypoLen = Math.sqrt(dx1*dx1+dy1*dy1);
double x1 = pointDownX-ARROW_LENGTH/hypoLen*dx1;
double y1 = pointDownY-ARROW_LENGTH/hypoLen*dy1;
double x2 = pointDownX-ARROW_LENGTH/hypoLen*dx2;
double y2 = pointDownY-ARROW_LENGTH/hypoLen*dy2;
canvas.drawLine(pointDownX,pointDownY, mLastX,mLastY, mBitmapPaint);
canvas.drawLine(pointDownX,pointDownY,(float)x1,(float)y1,mBitmapPaint);
canvas.drawLine(pointDownX,pointDownY,(float)x2,(float)y2,mBitmapPaint);
canvas.drawCircle(App.sScreenWidth/2-mWidth/2+dx,
App.sScreenHeight/2-mHeight/2+dy,mWidth/4,mBitmapPaint);
}
if (!isMove){
Moving();
}
canvas.restore();
invalidate();
}
还有一点就是碰撞之后物体的状态变化。这个扰了我好几天,最终实现的效果还不错。关键代码如下:
for (int i = 0;i< mList.size();i++)
for (int j =i ;j
if (i!=j && !mList.get(i).isKilled() && !mList.get(j).isKilled()
&&RectCollisionUtil.isCollision(mList.get(i),mList.get(j))){
AppIcon app1 = mList.get(i);
AppIcon app2 = mList.get(j);
int degree = app1.getDegree();
app1.setDegree(app2.getDegree());
app2.setDegree(degree);
while(RectCollisionUtil.isCollision(mList.get(i),mList.get(j))){
if (app1.getX().getX ()){
app1.setX((int) (app1.getX()-1));
app2.setX((int) (app2.getX()+1));
if (app1.getY().getY()){
app1.setY((int) (app1.getY()-1));
app2.setY((int) (app2.getY()+1));
}else {
app1.setY((int) (app1.getY()+1));
app2.setY((int) (app2.getY()-1));
}
}else {
app1.setX((int) (app1.getX()+1));
app2.setX((int) (app2.getX()-1));
if (app1.getY().getY()){
app1.setY((int) (app1.getY()-1));
app2.setY((int) (app2.getY()+1));
}else {
app1.setY((int) (app1.getY()+1));
app2.setY((int) (app2.getY()-1));
}
}
}
大概思路是这样,暴力遍历一遍运行的app,检测哪些碰撞了。如果碰撞了,交换速度。交换速度。交换速度!!然后,要在碰撞之后,在这一帧之内解决碰撞,那么就要分离两个物体。我的算法是每次给两个碰撞的物体位移一个像素,直到两者分离。
大概就是这样了。具体代码大家去我的github下载吧。
https://github.com/RuijiePan/Boom.git