大家好,我是皇叔,最近開了一個安卓進階漲薪訓練營,可以幫助大家突破技術&職場瓶頸,從而度過難關,進入心儀的公司。
詳情見文章:沒錯!皇叔開了個訓練營
作者:路很長OoOhttps://juejin.cn/post/7048623673892143140
本文帶大家看看自定義炫酷桌面小部件的各種技法。
一、AppWidgetProviderWidget是一種微型的應用程序視圖。可以嵌入到像桌面這樣的應用程序中,作為我們應用程序小功能的載體,由於Widget本身就是 BroadcastReceiver,且應用微件布局基於 RemoteViews,並不是每種布局或視圖微件都受其支持,目前只支持下面視圖類,如果需要其他支持View或者自定義View那麼需要framwork層添加容器:
以及以下微件類:
根據官網和文檔以及別人的博客可能我們僅僅能做的就是簡單的布局,列表,頂多來個🉑️拖拽的卡片或者能動起來的時鐘當然了我們應用自己的小部件很漂亮的。
抖音:沒錯安裝之後發現同樣的搜索框有6種樣式可選。




OneNote:設計簡約。

我們的日曆:挺好看的

這些市面上很多小部件都是基於應用常用的模塊提供快捷入口。並沒有進行很大的操作挖掘。當然了,Google這樣適配可能是為了避免對於cpu造成內存抖動,或者造成桌面性能等問題吧。但是作為開發者我們在乎的是我們產品的美觀和提高用戶的滿意度。所以動畫和自定義繪製還是很有必要的。
三、Widget動畫那我們還能不能搞那些Android 自定義View那些華麗呼哨的動畫呢?我的回答是必須可以,接下來我們逐步的進入正題,開始摸索。
1、創建Widget四步驟第一步 AndroidManifest.xml註冊前文我們說了Widget本身就是一個廣播接收者,當然也可以動態註冊。但是這裡我們需要清楚我們的小部件是依賴於桌面應用程序而不是我們的App,所以動態和靜態註冊需要我們考慮不同的產品需求,如果我們小部件不依賴於App宿主應用程序什麼周期,這種常駐類型的應用小部件靜態是首選吧。不明白的可以看看廣播靜態和動態註冊的區別。接下來我們在AndroidManifest靜態註冊:
<?xmlversion="1.0"encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"package="com.zui.recorder"><applicationandroid:name=".RecorderApplication"android:icon="@mipmap/ic_launcher_soundrecorder"android:label="@string/app_name"android:requestLegacyExternalStorage="true"android:resizeableActivity="true"android:supportsRtl="true"android:testOnly="false"android:theme="@style/AppBaseTheme"><!--name:是我們一會兒創建Widget的具體類--><receiverandroid:name=".ui.translation.widget.RecorderAppWidget"android:exported="true"><intent-filter><!--action:是用來更新我們小部件的行為動作標記--><actionandroid:name="android.appwidget.action.APPWIDGET_UPDATE"/></intent-filter><meta-dataandroid:name="android.appwidget.provider"android:resource="@xml/recorder_widget"/></receiver></application></manifest>第二步 定義應用微件的基本特性具體的參數看官網對應用微件的基本特性,字面意思應該差不多能讀懂,設置widget在桌面所占的空間大小限制,初始化視圖布局,以及更新Widget的時間,縮放方向模式等,如果設置最大限制這些參數沒有的可以將sdk升級到android 12也就是compileSdkVersion = 31即可。
<?xmlversion="1.0"encoding="utf-8"?><appwidget-providerxmlns:android="http://schemas.android.com/apk/res/android"android:description="@string/app_name"android:initialKeyguardLayout="@layout/widget_recorder_remote_view"android:initialLayout="@layout/widget_recorder_remote_view"android:minWidth="255dp"android:minHeight="100dp"android:minResizeWidth="255dp"android:minResizeHeight="100dp"android:previewImage="@drawable/blur_bg"android:resizeMode="horizontal|vertical"android:updatePeriodMillis="20000"android:widgetCategory="home_screen"/>第三步 定義應用微件的初始布局在 XML 中定義應用微件的初始布局,並將其保存在項目的 res/layout/目錄中。我們上文也提到了Widget布局基於 RemoteViews,並不是每種布局或視圖微件都受其支持。能支持的可以看上文或者官網。如下是我們今天完成的第一個視圖效果:
左邊是一個可以控制播放停止以及完成錄製的按鈕加一個錄音記時文字。右邊是一個跟隨錄音狀態可以跟隨這錄音進行波動動畫。到這裡是不是感覺到很醋,Widget也可以搞動畫麼?先看看我們的部件樣式。
<!--對於dimen這些以及命名自行規範,追求速度所以隨意命名和布局裡面寫死dp等--><?xmlversion="1.0"encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_margin="10dp"android:background="@drawable/widget_recorder_shape"android:elevation="10dp"android:orientation="vertical"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><LinearLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="10dp"android:layout_marginTop="10dp"android:layout_marginEnd="10dp"android:background="@drawable/widget_recorder_inner_shape"android:elevation="10dp"android:orientation="vertical"android:padding="5dp"><TextViewandroid:id="@+id/widget_title_text"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginStart="10dp"android:layout_marginTop="3dp"android:layout_marginEnd="10dp"android:layout_marginBottom="2dp"android:gravity="start"android:text="@string/app_name"android:textColor="@color/recorder_widget_title"android:textSize="14sp"/><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginStart="5dp"android:layout_marginEnd="20dp"android:orientation="horizontal"><!--左邊播放錄音和暫停錄音按鈕--><ImageViewandroid:id="@+id/widget_stop_bn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/notification_btn_pause"/><!--左邊結束錄音按鈕--><ImageViewandroid:id="@+id/widget_finish_bn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="20dp"android:background="@drawable/notification_finish"/></LinearLayout></LinearLayout><RelativeLayoutandroid:layout_width="0dp"android:layout_height="66dp"android:layout_marginTop="10dp"android:layout_marginEnd="10dp"android:layout_weight="1"android:background="@drawable/widget_recorder_inner_shape"android:padding="5dp"><!--右邊震動動畫--><ImageViewandroid:id="@+id/widget_wave"android:layout_width="wrap_content"android:layout_height="35dp"android:layout_centerInParent="true"android:layout_marginStart="20dp"android:scaleType="fitXY"/></RelativeLayout></LinearLayout></LinearLayout>第四步 繼承AppWidgetProviderAppWidgetProvider 類擴展了 BroadcastReceiver 作為一個輔助類來處理應用微件廣播。AppWidgetProvider 僅接收與應用微件有關的事件廣播,例如當更新、刪除、啟用和停用應用微件時發出的廣播。當發生這些廣播事件時,AppWidgetProvider 會接收並調用對應的 onUpdate()、onReceive()、onAppWidgetOptionsChanged、onDeleted、onRestored、onDisabled 等方法,具體方法可以看源碼或者官網詳解。接下來我們通過繼承 AppWidgetProvider 來實現我們的錄音機小部件類。
/**I*Createdbywangfei44on2021/12/28.*/classRecorderAppWidget:AppWidgetProvider(){overridefunonUpdate(context:Context?,appWidgetManager:AppWidgetManager?,appWidgetIds:IntArray?,){Log.i(TAG,"onUpdate")super.onUpdate(context,appWidgetManager,appWidgetIds)}overridefunonEnabled(context:Context){super.onEnabled(context)}overridefunonDisabled(context:Context){super.onDisabled(context)}overridefunonReceive(context:Context?,intent:Intent?){super.onReceive(context,intent)}}接下來我們將RecorderAppWidget註冊到清單文件<receiver android:name=".ui.translation.widget.RecorderAppWidget".../>
運行。長按桌面選擇我們小部件,效果如下:

對於應用層和小部件之間的交互刷新,我們可以通過廣播進行相互的刷新,數據的傳輸通過intent進行攜帶即可,例如當我錄製音頻時候,可以在服務裡面進行發送廣播,來傳遞數據並刷新小部件顯示錄製的時間或者其他的數據。相反點擊Widget的暫停和完成錄製按鈕也可以通過廣播通知錄音機服務進行更新應用的當前狀態。可以通過 AppwidgetManager 實時更新widget。
privateBroadcastReceiverwidgetBroadcastReceiver;privatevoidregisterWidgetReceiver(){if(null==widgetBroadcastReceiver){widgetBroadcastReceiver=newBroadcastReceiver(){@OverridepublicStringtoString(){return"$classname{}";}@OverridepublicvoidonReceive(Contextcontext,Intentintent){switch(intent.getAction()){caseACTION_CANCEL_TIMER:{if(isRecording()){//小部件通知過來了進行暫停錄音機進入pause狀態pauseRecording(true);//去刷新小部件圖標內容:sendBroadCastToRecorderWidget();}elseif(getState()==State.RECORD_PAUSED){resumeRecording(true);}break;}caseACTION_RESUME_TIMER:{//小部件通知過來了進行完成錄音進入IDLEstopRecording();break;}}}};}IntentFilterfilter=newIntentFilter();filter.addAction(ACTION_CANCEL_TIMER);filter.addAction(ACTION_RESUME_TIMER);try{registerReceiver(widgetBroadcastReceiver,filter);}catch(Exceptione){Logger.i("registerWidgetReceivererror:::$e");}}privatevoidunregisterWidgetReceiver(){if(widgetBroadcastReceiver==null){return;}try{unregisterReceiver(widgetBroadcastReceiver);}catch(java.lang.Exceptione){Logger.e("unregisterWidgetReceivererror:::$e");}widgetBroadcastReceiver=null;}//在RecorderService內部通過廣播高頻率的刷新小部件privatevoidsendBroadCastToRecorderWidget(){IntentupdateWidgetIntent=newIntent();//指定廣播行為動作的名字updateWidgetIntent.setAction(RecorderAppWidget.UPDATE_ACTION);//傳輸當前錄音機錄製的狀態updateWidgetIntent.putExtra(WIDGET_STATE_EXTRA_NAME,getState().ordinal());//傳輸當前錄音機錄製的時間updateWidgetIntent.putExtra(WIDGET_TIME_EXTRA_NAME,Utils.formatTime(getRecordingTime()));//發送廣播sendBroadcast(updateWidgetIntent);}//RecorderAppWidgetcompanionobject{constvalTAG="RecorderAppWidget"constvalUPDATE_ACTION="android.appwidget.action.APPWIDGET_UPDATE"//錄音機當前狀態和錄製時間constvalWIDGET_STATE_EXTRA_NAME="state"constvalWIDGET_TIME_EXTRA_NAME="time"//對應錄音機的錄製狀態constvalSTATE_IDLE=0constvalSTATE_PLAYING=1constvalSTATE_PLAY_PAUSED=2constvalSTATE_RECORDING=3constvalSTATE_RECORDING_FROM_PAUSED=4constvalSTATE_RECORD_PAUSED=5}//在RecorderAppWidget內部接收更具不同的狀態進行更新Widget視圖overridefunonReceive(context:Context,intent:Intent){this.context=contextsuper.onReceive(context,intent)Log.i(TAG,"onReceive")valremoteViews=RemoteViews(context.packageName,R.layout.widget_recorder_remote_view)valappWidgetIds=AppWidgetManager.getInstance(context).getAppWidgetIds(ComponentName(context,RecorderAppWidget::class.java))if(null==intent.action||UPDATE_ACTION!=intent.action){return}valtitleStart=getTitleStart(context,getState(intent))when(getState(intent)){STATE_RECORDING,STATE_RECORDING_FROM_PAUSED->{remoteViews.setTextViewText(R.id.widget_title_text,getTimeString(titleStart,context,intent))remoteViews.setImageViewResource(R.id.widget_stop_bn,R.drawable.notification_btn_pause)remoteViews.setWidgetOnClickPendingIntent(context,R.id.widget_stop_bn,ACTION_CANCEL_TIMER)remoteViews.setWidgetOnClickPendingIntent(context,R.id.widget_finish_bn,ACTION_RESUME_TIMER)remoteViews.setTextViewText(R.id.widget_time,getTimeString("",context,intent))remoteViews.setTextViewText(R.id.widget_time_center,getTimeString("",context,intent))}STATE_IDLE,STATE_RECORD_PAUSED->{updateAnimate(getState(intent))remoteViews.setTextViewText(R.id.widget_title_text,getTimeString(titleStart,context,intent))remoteViews.setImageViewResource(R.id.widget_stop_bn,R.drawable.notification_btn_resume)}}//由AppwidgetManager處理更新widgetvalawm=AppWidgetManager.getInstance(context.applicationContext)awm.updateAppWidget(appWidgetIds,remoteViews)}//獲取小部件對應的錄音機錄製狀態privatefungetState(intent:Intent):Int{returnintent.getIntExtra(WIDGET_STATE_EXTRA_NAME,STATE_IDLE)}//獲取錄音機錄製時間privatefungetTimeString(titleStart:String,context:Context,intent:Intent):String{vartime=intent.getStringExtra(WIDGET_TIME_EXTRA_NAME)if(null==time){time=""}if(time.isNotEmpty()){time="$titleStart$time"}returntime}privatefunRemoteViews.setWidgetOnClickPendingIntent(context:Context,id:Int,action:String,)=this.apply{setOnClickPendingIntent(id,PendingIntent.getBroadcast(context,0,Intent().setAction(action),PendingIntent.FLAG_IMMUTABLE))}到這裡我們基本的通知互刷搞定
3、Widget動畫實現大家想一想動畫的相關內容。什麼是動畫?接下來我們進入動畫的實現部分。動畫通俗點:順序播放一組圖片,大多數開發者應該都玩過幀動畫和補間動畫。對於動畫都流暢[FPS]當然就是我們單位秒數內出現的圖片[幀]數的多少來決定,來看看我們需要實現的這個動畫。看看右邊效果
Widget我們都是基於RemoteViews來刷新的,沒發通過View方式進行幀動畫來刷新。如何刷新RemoteViews呢?明白刷新原理我們也就有了實現的突破口,我們可以通過順序來刷新ImageView的資源也不就是動畫了麼?對於幀動畫的刷新一秒內刷新30幀左右應該看起來聽流暢的。接下來我們找UI要素材也就是每一幀的圖片。當然如果和我一樣寫demo你自己也可以製作幀素材。讓我們百度一個素材:
第一步GIF或者MP4素材、通過Gifski,MP4可以轉化為GIF;或者用Kap直接可以截取局部部分生成MP4或者GIF.

通過PhotoShop進行製作並導出幀圖片集合。打開GIF圖片之後,右邊通過shift來選擇所有的圖層



選中圖層所有圖片、快速導出為PNG。即可製作完成。

然後我們扔到drawble目錄。對於如何進行刷新圖片,當然是控制一定的時間段可以刷新視圖便可以完成。那我們可以通過那些方式進行刷新圖片呢?當然大家都會想到Handle和Runnable或者CountDownTimer[內部也是通過Handle實現的比較方便],當然了我們這裡使用來ValueAnimal進行更新,相對強於其他的,ValueAnimal刷新機制大家可以百度看看。
valIMAGES=arrayListOf(R.drawable.wave_animal_01,...R.drawable.wave_animal_55)//這裡我們設置動畫Value順序變化範圍數值為0到size-1也就是對應的圖片數組裡面圖片底0個到最後一張圖片。valvalueAnimator:ValueAnimator=ValueAnimator.ofInt(0,IMAGES.size-1)<br/>varduration=IMAGES.size*55LclassRecorderAppWidget:AppWidgetProvider(){companionobject{constvalTAG="RecorderAppWidget"constvalUPDATE_ACTION="android.appwidget.action.APPWIDGET_UPDATE"//錄音機當前狀態和錄製時間constvalWIDGET_STATE_EXTRA_NAME="state"constvalWIDGET_TIME_EXTRA_NAME="time"//對應錄音機的錄製狀態constvalSTATE_IDLE=0constvalSTATE_PLAYING=1constvalSTATE_PLAY_PAUSED=2constvalSTATE_RECORDING=3constvalSTATE_RECORDING_FROM_PAUSED=4constvalSTATE_RECORD_PAUSED=5varisFirst=truevarlastIndex=0valIMAGES=arrayListOf(R.drawable.wave_animal_01,R.drawable.wave_animal_02,R.drawable.wave_animal_03,R.drawable.wave_animal_04,R.drawable.wave_animal_05,R.drawable.wave_animal_06,R.drawable.wave_animal_07,R.drawable.wave_animal_08,R.drawable.wave_animal_09,R.drawable.wave_animal_10,R.drawable.wave_animal_11,R.drawable.wave_animal_12,R.drawable.wave_animal_13,R.drawable.wave_animal_14,R.drawable.wave_animal_15,R.drawable.wave_animal_16,R.drawable.wave_animal_17,R.drawable.wave_animal_18,R.drawable.wave_animal_19,R.drawable.wave_animal_20,R.drawable.wave_animal_21,R.drawable.wave_animal_22,R.drawable.wave_animal_23,R.drawable.wave_animal_24,R.drawable.wave_animal_25,R.drawable.wave_animal_26,R.drawable.wave_animal_27,R.drawable.wave_animal_28,R.drawable.wave_animal_30,R.drawable.wave_animal_31,R.drawable.wave_animal_32,R.drawable.wave_animal_33,R.drawable.wave_animal_34,R.drawable.wave_animal_35,R.drawable.wave_animal_36,R.drawable.wave_animal_37,R.drawable.wave_animal_38,R.drawable.wave_animal_39,R.drawable.wave_animal_40,R.drawable.wave_animal_41,R.drawable.wave_animal_42,R.drawable.wave_animal_43,R.drawable.wave_animal_44,R.drawable.wave_animal_45,R.drawable.wave_animal_46,R.drawable.wave_animal_47,R.drawable.wave_animal_48,R.drawable.wave_animal_49,R.drawable.wave_animal_50,R.drawable.wave_animal_51,R.drawable.wave_animal_52,R.drawable.wave_animal_54,R.drawable.wave_animal_55,R.drawable.wave_animal_56,R.drawable.wave_animal_57,)valvalueAnimator:ValueAnimator=ValueAnimator.ofInt(0,IMAGES.size-1)varduration=IMAGES.size*55L}privatelateinitvarcontext:ContextlateinitvarviewModel:SmartTranslationViewModeloverridefunonUpdate(context:Context?,appWidgetManager:AppWidgetManager?,appWidgetIds:IntArray?,){Log.i(TAG,"onUpdate")super.onUpdate(context,appWidgetManager,appWidgetIds)}//當Widget第一次創建的時候,該方法調用,然後啟動後台的服務overridefunonEnabled(context:Context){super.onEnabled(context)}//當把桌面上的Widget全部都刪掉的時候,調用該方法overridefunonDisabled(context:Context){super.onDisabled(context)}//我們在RecorderServierce裡面每秒鐘都會發送廣播,Widget的onReceive接收到之後進行刷新時間即可。overridefunonReceive(context:Context,intent:Intent){this.context=contextsuper.onReceive(context,intent)Log.i(TAG,"onReceive")valremoteViews=RemoteViews(context.packageName,R.layout.widget_recorder_remote_view)valappWidgetIds=AppWidgetManager.getInstance(context).getAppWidgetIds(ComponentName(context,RecorderAppWidget::class.java))if(null==intent.action||UPDATE_ACTION!=intent.action){return}valtitleStart=getTitleStart(context,getState(intent))when(getState(intent)){STATE_RECORDING,STATE_RECORDING_FROM_PAUSED->{remoteViews.setTextViewText(R.id.widget_title_text,getTimeString(titleStart,context,intent))remoteViews.setImageViewResource(R.id.widget_stop_bn,R.drawable.notification_btn_pause)remoteViews.setWidgetOnClickPendingIntent(context,R.id.widget_stop_bn,ACTION_CANCEL_TIMER)remoteViews.setWidgetOnClickPendingIntent(context,R.id.widget_finish_bn,ACTION_RESUME_TIMER)remoteViews.setTextViewText(R.id.widget_time,getTimeString("",context,intent))remoteViews.setTextViewText(R.id.widget_time_center,getTimeString("",context,intent))if(isFirst){updateAnimate(getState(intent))isFirst=false}}STATE_IDLE,STATE_RECORD_PAUSED->{updateAnimate(getState(intent))remoteViews.setTextViewText(R.id.widget_title_text,getTimeString(titleStart,context,intent))remoteViews.setImageViewResource(R.id.widget_stop_bn,R.drawable.notification_btn_resume)}}//由AppwidgetManager處理更新widgetvalawm=AppWidgetManager.getInstance(context.applicationContext)awm.updateAppWidget(appWidgetIds,remoteViews)}@SynchronizedprivatefunupdateWave(context:Context,index:Int){valremoteViews=RemoteViews(context.packageName,R.layout.widget_recorder_remote_view)valappWidgetIds=AppWidgetManager.getInstance(context).getAppWidgetIds(ComponentName(context,RecorderAppWidget::class.java))if(index!=lastIndex){lastIndex=indexremoteViews.setImageViewResource(R.id.widget_wave,IMAGES[index])remoteViews.setImageViewResource(R.id.item_content,IMAGES_CIRCLE[index])remoteViews.setImageViewResource(R.id.item_content_center,IMAGES_CIRCLE[index])}//由AppwidgetManager處理更新widgetvalawm=AppWidgetManager.getInstance(context.applicationContext)awm.updateAppWidget(appWidgetIds,remoteViews)}//根據狀態來更新文字前綴privatefungetTitleStart(context:Context,state:Int):String{returnif(state==STATE_RECORD_PAUSED){context.resources.getString(R.string.title_record_pause)}elseif(state==STATE_RECORDING||state==STATE_RECORDING_FROM_PAUSED){context.resources.getString(R.string.title_recording)}else{""}}//獲取小部件對應的錄音機錄製狀態privatefungetState(intent:Intent):Int{returnintent.getIntExtra(WIDGET_STATE_EXTRA_NAME,STATE_IDLE)}//獲取錄音機錄製時間privatefungetTimeString(titleStart:String,context:Context,intent:Intent):String{vartime=intent.getStringExtra(WIDGET_TIME_EXTRA_NAME)if(null==time){time=""}if(time.isNotEmpty()){time="$titleStart$time"}returntime}privatefunRemoteViews.setWidgetOnClickPendingIntent(context:Context,id:Int,action:String,)=this.apply{setOnClickPendingIntent(id,PendingIntent.getBroadcast(context,0,Intent().setAction(action),PendingIntent.FLAG_IMMUTABLE))}privatefunupdateAnimate(state:Int){Log.i("valueAnimator:value=",valueAnimator.toString())valueAnimator.repeatCount=INFINITEvalueAnimator.duration=durationvalueAnimator.repeatMode=RESTARTvalueAnimator.interpolator=LinearInterpolator()valueAnimator.addUpdateListener{updateWave(context,it.animatedValueasInt)}Log.i("state::==",state.toString())when(state){STATE_RECORDING,STATE_RECORDING_FROM_PAUSED->{if(valueAnimator.isPaused){valueAnimator.resume()}elseif(!valueAnimator.isRunning){valueAnimator.start()}}STATE_IDLE,STATE_RECORD_PAUSED->{valueAnimator.removeAllUpdateListeners()valueAnimator.pause()isFirst=true}}}}接下來運行結果:
同樣的我們實現水波圖不就缺少一個圖片數組麼?簡單同上面操作步驟找素材圖片。
到了這裡大家是不是覺得這種實現好像也不是很吊,通過幀圖片來實現這種效果。有本事你通過代碼寫一波小部件水波紋或者聲音波紋。當然了我們在這種比較高端的操作之前先來研究如何實現Widget的Canvas自定義吧。當我們能突破Widget的自定義之後,這種動畫實現起來也是沒啥問題的。接下來我們探索一下如何將Canvas引入到桌面小部件。
四、Widget自定義RemoteViews.setImageViewBitmap(id, bitmap)出現自然而然了解Canvas API並多用的開發者應該可以聯想到Canvas(@NonNull Bitmap bitmap) bitmap才是像素真正的載體,Canvas只是一個光柵畫布,我們花里胡哨的操作都最終會儲存在bitmap上並設置到視圖部件上。於是我們先繪製一條線?感受一波是否可行。
privatefundrawCanvas(remoteViews:RemoteViews,index:Int){valwidth=context.resources.getDimensionPixelSize(R.dimen.widget_canvas_width)valheight=context.resources.getDimensionPixelSize(R.dimen.widget_canvas_height)valbitmap=Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888)valcanvas=Canvas(bitmap)valpaint=Paint().apply{this.color=Color.argb(115,194,108,57)this.strokeWidth=2fthis.style=Paint.Style.STROKE}canvas.drawLine(0f,height/2f,width.toFloat(),height/2f,paint)remoteViews.setImageViewBitmap(R.id.widget_canvas,bitmap)}運行效果如下:

到這裡我們是不是找到了突破口,只要能加載bitmap那麼canvas自定義就是不是問題。
1.繪製之前的分析我們看到總共有三條骨架直線將屏幕分為六等分,我們可以簡單的求出三條線段的方程式吧?初中的數學我相信你能明白。
Yx=-tan30*x
Yx= tan30*x

同樣我們圓的半徑可以看做是各個骨架坐標軸的長度,而我們實際數據是長度數據而已如何將長度數字映射到各個不規則的骨架坐標軸上呢?當然還是離不開簡單的數學。
例如我們一個數字250如下圖兩個白色虛線相交地方。我們實際的250代表的是圓點到焦點部分的長度。但是我們需要在坐標系中定位那就需要求出(x,y)在坐標系中的虛擬坐標。同樣的簡單的初中數學,不難得出(x,y)=(lengthcson30,lenghtsin30),如果你細心分析每個骨架坐標軸上的所有坐標都滿足(x,y)=(lengthcson30,lenghtsin30)。接下來我們上代碼看效果

運行效果如下:

最終效果
到了這裡,我們可以任意自定義Widget,那麼水波紋和音頻抖動還用幀動畫來湊齊麼?當然了為了還原更加真實的水波紋和抖動動畫幀動畫只能是粗略的動而已,後面咋們來實現如何自定義水波紋和聲波動畫。



微信改了推送機制,真愛請星標本公號👇