English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
现在手机上的悬浮窗应用越来越多,对用户来说,最常见的悬浮窗应用就是安全软件的悬浮小控件,拿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); } //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; //このウィンドウの属性を設定します。 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、スマートフォンのスクリーン幅に基づいて記録します。/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; } params.x=0; } circleView.setDrageState(false); wm.updateViewLayout(circleView,params); if (Math.abs(moveX-downX>10){ return true; } 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.描画ペンを初期化し、ビューに対してタッチイベントを監視。タップおよびダブルタップイベントを監視。(ビューがタップ可能に設定されている必要があります)
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); } 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); } 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)のベセル曲線です。したがって、yを不断に変更することで、1の位置を利用して、水波の効果を描画できます。
まず、ダブルクリックイベントかどうかを判断します:
ダブルクリックされた場合:変数dを設定し、dの値を不断に変更することで(dの値の変更はnumによって引き起こされ、numはhandlerで不断に減少します。num--;)、ベ塞尔曲線を描画します。水波の下降効果を実現します。
クリックされた場合:count値を設定し、count値を不断に変更することで(count値の変更はhandlerで実現されます。count--;)、まずcountが割り切れるかどうかを判断します。2整数で割り切れる、これらのベジェ曲線を交互に描画します。(これらのベジェ曲線は逆対称です)。これにより、水の波の効果を実現します。
(forループを使って水の波の数を実現します。path.rQuadTo();は一対で波紋を1回だけ実現できます。自分で確認してください)
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); } } 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); } } 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.animation"360");//这里要判断是否为当前应用,要不然也可能会结束当前应用。 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="#F0"2F3942" 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="#c"93944" 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:第2引数は第1引数のタイプの開始値です(例えば、第1引数がAnimation.RELATIVE_TO_SELFに設定されている場合、第2引数は0。1f、これは自分の座標値に0を乗算することを意味します。1);
int toXType:x軸方向の終点の参照値には3オプションは第1引数と同じです。
float toValue:第4引数は第3引数のタイプの開始値です。
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(); } }
これで本文のすべての内容が終わりました。皆様の学習に役立つことを願っています。また、呐喊チュートリアルを多くの皆様にサポートしていただけると嬉しいです。
声明:本文の内容はインターネットから提供されています。著作権は原著者に帰属します。インターネットユーザーにより自発的に提供された内容であり、本サイトは所有権を有しません。人工編集は行われていません。また、関連する法的責任を負いません。著作権侵害を疑われる内容がある場合は、以下のメールアドレスまでご連絡ください:notice#oldtoolbag.com(メールを送信する際、#を@に置き換えてください。報告を行い、関連する証拠を提供してください。一旦確認が取れましたら、本サイトは即座に侵害を疑われるコンテンツを削除します。)