仿360加速球。(實現記憶體釋放)
現在手機上的懸浮窗應用越來越多,對使用者來說,最常見的懸浮窗應用就是安全軟體的懸浮小控制元件,拿360衛士來說,當開啟懸浮窗時,它是一個小球,小球可以拖動,當點選小球出現大窗體控制元件,可以進行進一步的操作如:釋放手機記憶體等等。於是藉著慕課網的視訊,仿著實現了360加速球,增加了點選小球進行釋放記憶體的功能。
由於是手機只有頻幕截圖:實現後如下圖所示:點選開啟按鈕,出現懸浮窗小球控制元件上面顯示手機的可用記憶體百分比;當拖動小球時,小球變為android圖示;鬆開小球,小球依附在頻幕兩側;點選小球,手機底部出現大窗體控制元件,點選裡面的小球,進行手機記憶體的釋放;點選手機螢幕的其他區域,大窗體消失,小球重新出現。
接下來就是實現的一些重要步驟:
1.FloatCircleView的實現(自定義view)
實現FloatCircleView的過程就是自定義view的過程。1、自定義View的屬性 2、在View的構造方法中獲得我們自定義的屬性 3、重寫onMesure 4、重寫onDraw。我們沒有自定義其他屬性所以省了好多步驟。
各種變數的初始化,設定拖動小球時要顯示的圖示,已經計算各種記憶體。(用於顯示在小球上)
public int width=100;
public int heigth=100;
private Paint circlePaint;//畫圓
private Paint textPaint; //畫字
private float availMemory; //已用記憶體
private float totalMemory; //總記憶體
private String text; //顯示的已用記憶體百分比
private boolean isDraging=false; //是否在拖動狀態。
private Bitmap src;
private Bitmap scaledBitmap; //縮放後的圖片。
/**
* 初始化畫筆以及計算可用記憶體,總記憶體,和可用記憶體百分比。
*/
public void initPatints() {
circlePaint = new Paint();
circlePaint.setColor(Color.CYAN);
circlePaint.setAntiAlias(true);
textPaint = new Paint();
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(25);
textPaint.setFakeBoldText(true);
textPaint.setAntiAlias(true);
//設定圖片
src = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
//縮放後的圖片(將圖示設定的和懸浮小球一樣大小。)
scaledBitmap = Bitmap.createScaledBitmap(src, width, heigth, true);
//計算已用記憶體,總記憶體,已用記憶體百分比,
availMemory= (float) getAvailMemory(getContext());
totalMemory= (float) getTotalMemory(getContext());
text=(int)((availMemory/totalMemory)*100)+"%";
}
onMeasure();就是將固定的寬高寫死,通過
setMeasuredDimension(width, heigth);
傳入。
onDraw();進行懸浮小球繪製。定義一個boolean變數判斷當前狀態是否為拖動小球狀態,如果是拖動小球狀態,就在該位置繪製android圖示,如果不是拖動狀態,就進行小球繪製。畫小球沒有難度,關鍵是畫字。下面的2個圖可以加深對畫字時的理解。
1.畫字時的x座標(1.textPaint.measureText(text);得到字的寬度
2.小球的寬度/2-字的寬度/2。)
2.畫字時的y座標(1.Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
得到字型屬性測量類。2.(fontMetrics.ascent + fontMetrics.descent) / 2
得到字的高度。3.小球的高度/2-字型的高度/2)
畫個圖就很好理解了:
/**
* 畫小球及文字。如果小球是在拖動狀態就顯示android圖示,如果不是拖動狀態就顯示小球。
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
if (isDraging){
canvas.drawBitmap(scaledBitmap,0,0,null);
}else {
//1.畫圓
canvas.drawCircle(width / 2, heigth / 2, width / 2, circlePaint);
//2.畫text
float textwidth = textPaint.measureText(text);//文字寬度
float x = width / 2 - textwidth / 2;
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float dy = -(fontMetrics.ascent + fontMetrics.descent) / 2;
float y = heigth / 2 + dy;
canvas.drawText(text, x, y, textPaint);
}
}
獲得手機已用記憶體及總記憶體的方法:
public long getAvailMemory(Context context)
{
// 獲取android當前可用記憶體大小
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
am.getMemoryInfo(mi);
//mi.availMem; 當前系統的可用記憶體
//return Formatter.formatFileSize(context, mi.availMem);// 將獲取的記憶體大小規格化
return mi.availMem/(1024*1024);
}
public long getTotalMemory(Context context)
{
String str1 = "/proc/meminfo";// 系統記憶體資訊檔案
String str2;
String[] arrayOfString;
long initial_memory = 0;
try
{
FileReader localFileReader = new FileReader(str1);
BufferedReader localBufferedReader = new BufferedReader(
localFileReader, 8192);
str2 = localBufferedReader.readLine();// 讀取meminfo第一行,系統總記憶體大小
arrayOfString = str2.split("\\s+");
for (String num : arrayOfString) {
Log.i(str2, num + "\t");
}
initial_memory = Integer.valueOf(arrayOfString[1]).intValue() * 1024;// 獲得系統總記憶體,單位是KB,乘以1024轉換為Byte
localBufferedReader.close();
} catch (IOException e) {
}
//return Formatter.formatFileSize(context, initial_memory);// Byte轉換為KB或者MB,記憶體大小規格化
return initial_memory/(1024*1024);
}
2.建立WindowManager窗體管理類,管理懸浮小球和底部大窗體。
WindowManager類。用來管理整個懸浮小球和手機底部大窗體的顯示和隱藏。
必須在Manifest檔案中增加<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
許可權。
通過WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
獲取窗體管理類;
利用wm.addView(view, params);
將view增加到窗體中。
利用wm.remove(view,params);
將view從窗體中移除。
利用wm.updateViewLayout(view,params);
來更新view.
WindowManager.LayoutParams
用來設定view的各種屬性。1.建立FloatViewManager例項。
//單例模式建立
public static FloatViewManager getInstance(Context context){
if (inStance==null){
synchronized(FloatViewManager.class){
if (inStance==null){
inStance=new FloatViewManager(context);
}
}
}
return inStance;
}
2.展示懸浮小球和展示底部窗體的方法。(展示窗體的方法同展示懸浮小球類似。)
/**
* 展示浮窗
*/
public void showFloatCircleView(){
//引數設定
if (params==null){
params = new WindowManager.LayoutParams();
//寬高
params.width=circleView.width;
params.height=circleView.heigth;
//對齊方式
params.gravity= Gravity.TOP|Gravity.LEFT;
//偏移量
params.x=0;
params.y=0;
//型別
params.type=WindowManager.LayoutParams.TYPE_TOAST;
//設定該window屬性。
params.flags= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
//畫素格式
params.format= PixelFormat.RGBA_8888;
}
//將小球加入窗體中。
wm.addView(circleView, params);
}
public void showFloatCircleView(){
......
}
3.當啟動程式,首先建立懸浮小球,小球可以拖拽,點選小球,手機底部窗體顯示(
FloatMenuView
),小球隱藏。所以,對小球(circleView)要對其進行setOnTouchListener
和setOnClickListener
事件監聽。
分析小球的事件分發; 對於小球:
當ACTION_DOWN時,記錄小球的downX,downY,以及startX,startY,
當ACTION_MOVE時,將circleView是否拖拽狀態置為true,記錄小球的moveX,moveY,計算小球移動的距離(dx,dy),然後根據wm.updateViewLayout(circleView,params);
更新小球位置。最後將最後move的座標賦值給startX,startY。
當ACTION_UP時,將circleView是否拖拽置為false,記錄擡起時的座標,upx,根據upx和手機螢幕寬度/2,進行判斷,來覺得最終小球是貼在螢幕左側,還是右側。後面為小球拖拽的誤差。當小球拖拽的距離小於10個畫素時,可以觸發小球的點選事件。(小球的Touch事件,優先於小球的點選事件,當Touch事件返回true時,此事件被消費,不再向下傳遞事件。當Touch事件返回false時,此事件繼續向下傳遞,從而觸發小球的點選事件。)
小球的點選事件:點選小球,懸浮小球隱藏,手機底部窗體出現。並設定有底部窗體出現時的過渡動畫。
//給circleView設定touch監聽。
private View.OnTouchListener circleViewOnTouchListener=new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//最後按下時的座標,根據ACTION_MOVE理解。
startX = event.getRawX();
startY = event.getRawY();
//按下時的座標。
downX = event.getRawX();
downY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
circleView.setDrageState(true);
moveX = event.getRawX();
moveY=event.getRawY();
float dx = moveX -startX;
float dy=moveY-startY;
params.x+=dx;
params.y+=dy;
wm.updateViewLayout(circleView,params);
startX= moveX;
startY=moveY;
break;
case MotionEvent.ACTION_UP:
float upx=event.getRawX();
if (upx>getScreenWidth()/2){
params.x=getScreenWidth()-circleView.width;
}else {
params.x=0;
}
circleView.setDrageState(false);
wm.updateViewLayout(circleView,params);
if (Math.abs(moveX-downX)>10){
return true;
}else {
return false;
}
default:
break;
}
return false;
}
};
circleView.setOnTouchListener(circleViewOnTouchListener);
circleView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//Toast.makeText(, "onclick", Toast.LENGTH_SHORT).show();
//隱藏circleView,顯示選單欄。
wm.removeView(circleView);
showFloatMenuView();
floatMenuView.startAnimation();
}
});
3.MyProgreeView(手機底部窗體中小球的實現)。
1.初始化畫筆,對view進行手勢監聽。監聽單擊和雙擊事件。(必須設定view是可以點選的)
private void initPaint() {
//畫圓畫筆
circlepaint = new Paint();
circlepaint.setColor(Color.argb(0xff, 0x3a, 0x8c, 0x6c));
circlepaint.setAntiAlias(true);
//畫進度條畫筆
progerssPaint = new Paint();
progerssPaint.setAntiAlias(true);
progerssPaint.setColor(Color.argb(0xff, 0x4e, 0xcc, 0x66));
progerssPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));//繪製重疊部分
//畫進度畫筆
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(25);
//畫布
bitmap = Bitmap.createBitmap(width, heigth, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
//手勢監聽。
gestureDetector = new GestureDetector(new MyGertureDetectorListener());
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
});
//設定view可以點選。
setClickable(true);
}
class MyGertureDetectorListener extends GestureDetector.SimpleOnGestureListener{
@Override
public boolean onDoubleTap(MotionEvent e) {
......
//雙擊事件的邏輯
return super.onDoubleTap(e);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
......
//單擊事件的邏輯
return super.onSingleTapConfirmed(e);
}
}
2.用handler互動進行單擊和雙擊事件的狀態更新。單擊時,利用貝塞爾曲線,實現波紋盪漾效果。雙擊時,波紋不斷下降,進行記憶體釋放,最後顯示記憶體釋放後的已用記憶體百分比。handler傳送週期訊息,讓單擊事件和雙擊事件的小球不斷進行重繪。(重繪在下一小節講)。
//單擊事件傳送週期handler.
private void startSingleTapAnimation() {
handler.postDelayed(singleTapRunnable,200);
}
private SingleTapRunnable singleTapRunnable=new SingleTapRunnable();
class SingleTapRunnable implements Runnable{
@Override
public void run() {
count--;
if (count>=0) {
invalidate();//不斷進行重繪。
handler.postDelayed(singleTapRunnable,200);
}else {
handler.removeCallbacks(singleTapRunnable);
count=50;
}
}
}
//雙擊事件傳送週期handler。
private void startDoubleTapAnimation() {
handler.postDelayed(runnbale,50);
}
private DoubleTapRunnable runnbale=new DoubleTapRunnable();
class DoubleTapRunnable implements Runnable{
@Override
public void run() {
num--;
if (num>=0){
invalidate();//不斷進行重繪。
handler.postDelayed(runnbale,50);
}else {
handler.removeCallbacks(runnbale);
//釋放記憶體。
killprocess();
//計算釋放後的已用記憶體百分比。
num=(int)(((float)currentProgress/max)*100);
}
}
}
3.單擊事件和雙擊事件的重繪。
首先是小球的繪製,和波紋路徑的繪製。
//繪製小球
bitmapCanvas.drawCircle(width / 2, heigth / 2, width / 2, circlepaint);
//根據path,繪製波紋路徑。每次繪製前將上次的path,reset.
path.reset();
float y =(1-(float)num/100)*heigth;
path.moveTo(width, y);
path.lineTo(width, heigth);
path.lineTo(0, heigth);
path.lineTo(0, y);
接著利用貝塞爾曲線將波紋路徑繪製。
Android-貝塞爾曲線
貝塞爾曲線在android中的應用
這裡有詳細的講解貝塞爾曲線。其實不需要深入的理解。只要知道能用它來實現水波紋效果就行了(貝塞爾曲線用處很多,翻書效果也可以用它實現。)主要利用path.rQuadTo(x1,y1,x2,y2);
終點(x2,y2),輔助控制點(x1,y1)的貝塞爾曲線。因此,通過不斷改變y1的位置,我們可以繪製出水波紋的效果。
首先判斷它是否為雙擊擊事件:
若是雙擊:設定一個變數d,通過不斷改變d的值(d的值的改變由num引起,而num實在handler中不斷減小的。num–;),來繪製貝塞爾曲線。實現水波紋的下降效果。
若是單擊:設定一個count值,通過不斷改變count值(count值的改變是在handler中實現的。count–;),首先判斷count是否能被2整除,交替繪製這兩條貝塞爾曲線。(這兩條貝塞爾曲線正好相反),從而實現水波盪漾的效果。
(用for迴圈是實現水波的波數,一對path.rQuadTo();只能實現一次波紋。可以自己去驗證)
if (!isSingleTap){
float d=(1-(float)num/(100/2))*10;
for (int i=0;i<3;i++){
path.rQuadTo(10,-d,20,0);
path.rQuadTo(10,d,20,0);
}
}else {
float d=(float)count/50*10;
if (count%2==0){
for (int i=0;i<=3;i++){
path.rQuadTo(10,-d,30,0);
path.rQuadTo(10,d,30,0);
}
}else {
for (int i=0;i<=3;i++){
path.rQuadTo(10,d,30,0);
path.rQuadTo(10,-d,30,0);
}
}
}
最後是釋放記憶體的方法。記得要在Manifest檔案中增加
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
許可權。
public void killprocess(){
ActivityManager activityManger=(ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> list=activityManger.getRunningAppProcesses();
if(list!=null)
for(int i=0;i<list.size();i++)
{
ActivityManager.RunningAppProcessInfo apinfo=list.get(i);
String[] pkgList=apinfo.pkgList;
if(apinfo.importance>ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE)
{
// Process.killProcess(apinfo.pid);
for(int j=0;j<pkgList.length;j++) {
boolean flag=pkgList[j].contains("com.example.yyh.animation360");//這裡要判斷是否為當前應用,要不然也可能會結束當前應用。
if(!flag){
activityManger.killBackgroundProcesses(pkgList[j]);
}
}
}
}
4.FloatMenuView的實現。
1.建立一個float_menuview.xml;其中包括一個ImageView+TextView+自定義的MyProgreeView。
底部窗體要被設定能被點選。android:clickable="true"
;
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#33000000"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#F02F3942"
android:layout_alignParentBottom="true"
android:id="@+id/ll"
android:clickable="true"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@mipmap/ic_launcher"
android:layout_gravity="center_vertical"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="15sp"
android:textColor="#c93944"
android:text="360加速球"
android:layout_gravity="center_vertical"
/>
</LinearLayout>
<com.example.yyh.animation360.view.MyProgreeView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
/>
</LinearLayout>
</RelativeLayout>
2.將FloatMenuView 根據條件,利用(
wm.addView(view, params);
將view增加到窗體中。
利用wm.remove(view,params);
將view從窗體中移除。)方法,進行底部窗體view的顯示和隱藏
TranslateAnimation
類用來設定底部窗體進入時的動畫效果。TranslateAnimation(int fromXType,float fromXValue,int toXType,float toXValue,int fromYType,float fromYValue,int toYType,float toYValue)
int fromXType:x軸方向起始的參照值有3個選項。(1.Animation.ABSOLUTE
:具體的座標值,指絕對的螢幕畫素單位。2.Animation.RELATIVE_TO_SELF
:相對自己的座標值。3.Animation.RELATIVE_TO_PARENT
:相對父容器的座標值。)
float fromXValue 第二個引數是第一個引數型別的起始值(例如若第一個引數設定為Animation.RELATIVE_TO_SELF,第二個引數為0.1f,就表示為自己的座標值乘以0.1);
int toXType:x軸方向終點的參照值有3個選項同第一個引數。
float toValue:第四個引數是第三個引數型別的起始值。
Y軸方向的引數同理。起點+終點;(每個引數後一個引數為前一個引數的起始值。)
並對此view設定OnTouchListener,OnTouch事件最後必須返回false,表示此事件仍然需要向下傳遞。從而實現點選手機其他區域時,手機底部窗體隱藏,懸浮小球顯示,點選底部窗體時無變化,點選底部窗體中的小球時,觸發其單擊和雙擊事件。
View view =View.inflate(getContext(), R.layout.float_menuview,null);
LinearLayout linearLayout= (LinearLayout) view.findViewById(R.id.ll);
translateAnimation = new TranslateAnimation(Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,1.0f,Animation.RELATIVE_TO_SELF,0);
translateAnimation.setDuration(500);
translateAnimation.setFillAfter(true);
linearLayout.setAnimation(translateAnimation);
view.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
FloatViewManager manager=FloatViewManager.getInstance(getContext());
manager.hideFloatMenuView();
manager.showFloatCircleView();
return false;
}
});
addView(view);
5.MyFloatService
用來建立FloatVIewManager單例,管理懸浮小球+手機底部窗體的建立和移除。
public class MyFloatService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
//用來開啟FloatViewManager
FloatViewManager manager=FloatViewManager.getInstance(this);
manager.showFloatCircleView();
super.onCreate();
}
}
6.MainActivity的實現
定義一個intent,開啟服務(在服務中建立WindowManager單例物件,進行懸浮小球和手機底部窗體的管理。),關閉當前的activity。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void startService(View view){
Intent intent=new Intent(this, MyFloatService.class);
startService(intent);
finish();
}
}