如何步入小部件的自定义,搞定这节课
发表时间:2023-09-02 07:00:41
文章来源:炫佑科技
浏览次数:159
菏泽炫佑科技
如何步入小部件的自定义,搞定这节课
*近,相信大家都坐在小板凳上听着MIUI的内容。 当然,MIUI小部件可以说是对MIUI的提升很大。 作为一名开发者,你是否对此感到惊讶? 当然,我也见过很多看似定制化的简单操作。 当然,大多数都是可爱简单的背景,带有动画。 ,并附有定制内容。 不管小米自己开发了布局还是应用定制,我想只要我们能定制动画就行了,更何况是简单的动画。 没有什么是你想不到的,也没有什么是你想象不到的。 接下来我们就来看看如何一步步进行的定制。 学完这节课,你的小部件超越小米和IOS只是UI的问题。
一,
是一个微型应用程序视图。 它可以嵌入到桌面这样的应用程序中,作为我们应用程序小功能的载体。 由于它本身,并且应用程序小部件的布局基于它,因此并非每个布局或视图小部件都受它支持。 目前仅支持以下视图。 Class,如果需要View或者自定义View的其他支持,则需要在容器中添加一层:
- `FrameLayout`
- `LinearLayout`
- `RelativeLayout`
- `GridLayout`
以及以下微件类:
- `AnalogClock`
- `Button`
- `Chronometer`
- `ImageButton`
- `ImageView`
- `ProgressBar`
- `TextView`
- `ViewFlipper`
- `ListView`
- `GridView`
- `StackView`
- `AdapterViewFlipper`
2. 目前的共同愿景
根据官网、文档和别人的博客,也许我们能做的只是一个简单的布局,一个列表,顶多是一个️可拖动的卡片或者一个可以移动的时钟。 当然,我们自己的小部件非常漂亮。
抖音:是的,安装后我发现同一个搜索框有6种样式可供选择。
:简单的设计。
我们的日历:非常好
市场上的许多小部件都提供基于应用程序中常用模块的快速访问。 没有进行大规模的作业挖掘。 当然,这样的适配可能是为了避免给CPU带来内存抖动,或者造成桌面性能等问题。 但作为开发者,我们关心的是产品的美观和提高用户满意度。 所以动画和自定义绘图还是有必要的。
3. 无法动画? 定制?
那我们还可以对自定义View进行那些华丽的哨子操作吗? 动画不是很棒吗? 自定义View是不是很漂亮? 这些可以做到吗? 没有上层的帮助【毕竟很多开发者是不可能修改系统底层的,除非有自己的系统配方】,就在现有的API上摸索、上线。 今天我们就一步一步来分析,由浅入深,每个人的发展年限都不同,所以希望大家能够理解我。 毕竟我写的文章比较啰嗦。 希望跟我一样经历过的人也能理解。
我们对桌面上的以下动画和自定义内容有什么想法吗? 大多数阅读官网或编写小部件的开发者都否决了它。 那么下面的动画和定制可以做吗? 我的回答是一定可以,然后我们就逐渐进入正题,开始探索。
1.动画:
View中两个好看的动画列表
水波动画
声音动画
2. 定制:
View中一些好看的统计图表
折线图
K线图
对于那些使用自定义 API 太少的人,可以阅读我的这些自定义文章。 那个时候真的很辛苦。 希望这可以帮助。 顺便说一句,点个赞也不错。 以前的一个小兄弟看了我的文章很兴奋,每篇文章都给我点赞,加好友如何步入小部件的自定义,搞定这节课,各种夸奖我,好久没有联系了,前天突然问我关于我的网站上的折线图定制功能做不到!!!
自定义 - 曲线渐变填充
自定义 - 任何区域均可点击折线图
自定义-手势缩放折线图
自定义-手势滑动缩放渐变填充曲线折线图
-基本布局
- - 定制图纸
- - 动态用户界面?
- 用户界面结束
- 水墨画效果
4. 动画
让我们看看如何轻松创建一个。 现在官方的API也已经分包了。 如果你想入手的话可以看看我对面老大的文章。 每个人都能读懂吗? 小部件的春天来了,当然买个他写的新UI编程不是很好吗? 。 前两个动画是给大家准备的。 下面的动画就是我们今天要实现的动画模块的内容。 声波、水波效果动画。
1. 创建四个步骤
"1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.zui.recorder">
<application
android: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">
<receiver
android:name=".ui.translation.widget.RecorderAppWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/recorder_widget" />
receiver>
application>
manifest>
"1.0" encoding="utf-8"?>
<appwidget-provider xmlns: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" />
"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:layout_margin="10dp"
android:background="@drawable/widget_recorder_shape"
android:elevation="10dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android: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">
<TextView
android: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" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="20dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/widget_stop_bn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/notification_btn_pause" />
<ImageView
android: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>
<RelativeLayout
android: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">
<ImageView
android: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>
/**I
* Created by wangfei44 on 2021/12/28.
*/
class RecorderAppWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetIds: IntArray?,
) {
Log.i(TAG, "onUpdate")
super.onUpdate(context, appWidgetManager, appWidgetIds)
}
override fun onEnabled(context: Context) {
super.onEnabled(context)
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
}
override fun onReceive(context: Context?, intent: Intent?) {
super.onReceive(context, intent)
}
}
接下来我们将注册到文件中
跑步。 长按桌面选择我们的小部件,效果如下:
2. 与应用程序的通讯和刷新
对于应用层和之间的交互刷新,我们可以通过广播的方式互相刷新,并且可以进行数据传输。 例如,当我录制音频时,我可以在服务中发送广播来传输数据并刷新小部件显示录制时间或其他数据。 反向单击“暂停”和“完成录制”按钮还会向录制器服务广播通知,以更新应用程序的当前状态。
可以实时更新。
private BroadcastReceiver widgetBroadcastReceiver;
private void registerWidgetReceiver() {
if (null == widgetBroadcastReceiver) {
widgetBroadcastReceiver = new BroadcastReceiver() {
@Override
public String toString() {
return "$classname{}";
}
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case ACTION_CANCEL_TIMER: {
if (isRecording()) {
//小部件通知过来了进行暂停录音机进入pause状态
pauseRecording(true);
//去刷新小部件图标内容:
sendBroadCastToRecorderWidget();
} else if (getState() == State.RECORD_PAUSED) {
resumeRecording(true);
}
break;
}
case ACTION_RESUME_TIMER: {
//小部件通知过来了进行完成录音进入IDLE
stopRecording();
break;
}
}
}
};
}
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_CANCEL_TIMER);
filter.addAction(ACTION_RESUME_TIMER);
try {
registerReceiver(widgetBroadcastReceiver, filter);
} catch (Exception e) {
Logger.i("registerWidgetReceiver error ::: $e");
}
}
private void unregisterWidgetReceiver() {
if (widgetBroadcastReceiver == null) {
return;
}
try {
unregisterReceiver(widgetBroadcastReceiver);
} catch (java.lang.Exception e) {
Logger.e("unregisterWidgetReceiver error ::: $e");
}
widgetBroadcastReceiver = null;
}
//在RecorderService内部通过广播高频率的刷新小部件
private void sendBroadCastToRecorderWidget() {
Intent updateWidgetIntent = new Intent();
//指定广播行为动作的名字
updateWidgetIntent.setAction(RecorderAppWidget.UPDATE_ACTION);
//传输当前录音机录制的状态
updateWidgetIntent.putExtra(WIDGET_STATE_EXTRA_NAME,getState().ordinal());
//传输当前录音机录制的时间
updateWidgetIntent.putExtra(WIDGET_TIME_EXTRA_NAME, Utils.formatTime(getRecordingTime()));
//发送广播
sendBroadcast(updateWidgetIntent);
}
//RecorderAppWidget
companion object {
const val TAG = "RecorderAppWidget"
const val UPDATE_ACTION = "android.appwidget.action.APPWIDGET_UPDATE"
//录音机当前状态和录制时间
const val WIDGET_STATE_EXTRA_NAME = "state"
const val WIDGET_TIME_EXTRA_NAME = "time"
//对应录音机的录制状态
const val STATE_IDLE = 0
const val STATE_PLAYING = 1
const val STATE_PLAY_PAUSED = 2
const val STATE_RECORDING = 3
const val STATE_RECORDING_FROM_PAUSED = 4
const val STATE_RECORD_PAUSED = 5
}
//在RecorderAppWidget内部接收更具不同的状态进行更新Widget视图
override fun onReceive(context: Context, intent: Intent) {
this.context = context
super.onReceive(context, intent)
Log.i(TAG, "onReceive")
val remoteViews = RemoteViews(context.packageName, R.layout.widget_recorder_remote_view)
val appWidgetIds = AppWidgetManager.getInstance(context)
.getAppWidgetIds(
ComponentName(
context,
RecorderAppWidget::class.java
)
)
if (null == intent.action || UPDATE_ACTION != intent.action) {
return
}
val titleStart = 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 处理更新widget
val awm = AppWidgetManager.getInstance(context.applicationContext)
awm.updateAppWidget(appWidgetIds, remoteViews)
}
//获取小部件对应的录音机录制状态
private fun getState(intent: Intent): Int {
return intent.getIntExtra(WIDGET_STATE_EXTRA_NAME, STATE_IDLE)
}
//获取录音机录制时间
private fun getTimeString(titleStart: String, context: Context, intent: Intent): String {
var time = intent.getStringExtra(WIDGET_TIME_EXTRA_NAME)
if (null == time) {
time = ""
}
if (time.isNotEmpty()) {
time = "$titleStart $time"
}
return time
}
private fun RemoteViews.setWidgetOnClickPendingIntent(
context: Context,
id: Int,
action: String,
) = this.apply {
setOnClickPendingIntent(id, PendingIntent
.getBroadcast(
context, 0, Intent().setAction(action),
PendingIntent.FLAG_IMMUTABLE
))
}
这里我们得到了基本的通知相互刷卡
3. 动画实现
我们来思考一下动画的相关内容。 什么是动画? 接下来我们进入动画的实现部分。 一般动画:按顺序播放一组图片。 大多数开发者应该都玩过帧动画和补间动画。 对于流畅的动画【FPS】来说,当然是由我们以秒为单位出现的图片【帧数】的数量来决定的。 我们看一下我们需要实现的动画,看右边的效果
我们都是以刷新为主,并没有通过View使用帧动画来刷新。 怎么刷新呢? 理解刷新原理,让我们在实现上有了突破。 我们可以通过序列刷新的资源不就是动画吗? 对于帧动画的刷新来说,一秒刷新30帧左右应该看起来比较流畅。 接下来我们寻找UI素材,就是每一帧的图片。 当然,如果你像我一样写demo,也可以自己制作框架素材。 让我们百度一下资料:
然后使用巨型选框工具选择需要的部分,使用图像->裁剪进行整体裁剪或修剪。
选择该图层的所有图片并快速导出为PNG。 生产完成。
然后我们进入目录。
至于如何刷新图片,当然可以通过控制一定时间刷新视图来完成。 那么我们可以使用哪些方法来刷新图像呢? 当然大家都会想到and or【内部实现也比较方便】。 当然我们这里用它来更新,相对来说比其他的要强一些。 您可以在百度上查看刷新机制。
val IMAGES = arrayListOf( R.drawable.wave_animal_01, ...R.drawable.wave_animal_55)
//这里我们设置动画Value顺序变化范围数值为0到size-1也就是对应的图片数组里面图片底0个到*后一张图片。
val valueAnimator: ValueAnimator = ValueAnimator.ofInt(0, IMAGES.size - 1)
var duration = IMAGES.size * 55L
class RecorderAppWidget : AppWidgetProvider() {
companion object {
const val TAG = "RecorderAppWidget"
const val UPDATE_ACTION = "android.appwidget.action.APPWIDGET_UPDATE"
//录音机当前状态和录制时间
const val WIDGET_STATE_EXTRA_NAME = "state"
const val WIDGET_TIME_EXTRA_NAME = "time"
//对应录音机的录制状态
const val STATE_IDLE = 0
const val STATE_PLAYING = 1
const val STATE_PLAY_PAUSED = 2
const val STATE_RECORDING = 3
const val STATE_RECORDING_FROM_PAUSED = 4
const val STATE_RECORD_PAUSED = 5
var isFirst = true
var lastIndex = 0
val IMAGES = 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,
)
val valueAnimator: ValueAnimator = ValueAnimator.ofInt(0, IMAGES.size - 1)
var duration = IMAGES.size * 55L
}
private lateinit var context: Context
lateinit var viewModel: SmartTranslationViewModel
override fun onUpdate(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetIds: IntArray?,
) {
Log.i(TAG, "onUpdate")
super.onUpdate(context, appWidgetManager, appWidgetIds)
}
//当Widget**次创建的时候,该方法调用,然后启动后台的服务
override fun onEnabled(context: Context) {
super.onEnabled(context)
}
//当把桌面上的Widget全部都删掉的时候,调用该方法
override fun onDisabled(context: Context) {
super.onDisabled(context)
}
//我们在RecorderServierce里面每秒钟都会发送广播,Widget的onReceive接收到之后进行刷新时间即可。
override fun onReceive(context: Context, intent: Intent) {
this.context = context
super.onReceive(context, intent)
Log.i(TAG, "onReceive")
val remoteViews = RemoteViews(context.packageName, R.layout.widget_recorder_remote_view)
val appWidgetIds = AppWidgetManager.getInstance(context)
.getAppWidgetIds(
ComponentName(
context,
RecorderAppWidget::class.java
)
)
if (null == intent.action || UPDATE_ACTION != intent.action) {
return
}
val titleStart = 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 处理更新widget
val awm = AppWidgetManager.getInstance(context.applicationContext)
awm.updateAppWidget(appWidgetIds, remoteViews)
}
@Synchronized
private fun updateWave(context: Context, index: Int) {
val remoteViews = RemoteViews(context.packageName, R.layout.widget_recorder_remote_view)
val appWidgetIds = AppWidgetManager.getInstance(context)
.getAppWidgetIds(
ComponentName(
context,
RecorderAppWidget::class.java
)
)
if (index != lastIndex) {
lastIndex = index
remoteViews.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 处理更新widget
val awm = AppWidgetManager.getInstance(context.applicationContext)
awm.updateAppWidget(appWidgetIds, remoteViews)
}
//根据状态来更新文字前缀
private fun getTitleStart(context: Context, state: Int): String {
return if (state == STATE_RECORD_PAUSED) {
context.resources.getString(R.string.title_record_pause)
} else if (state == STATE_RECORDING || state == STATE_RECORDING_FROM_PAUSED) {
context.resources.getString(R.string.title_recording)
} else {
""
}
}
//获取小部件对应的录音机录制状态
private fun getState(intent: Intent): Int {
return intent.getIntExtra(WIDGET_STATE_EXTRA_NAME, STATE_IDLE)
}
//获取录音机录制时间
private fun getTimeString(titleStart: String, context: Context, intent: Intent): String {
var time = intent.getStringExtra(WIDGET_TIME_EXTRA_NAME)
if (null == time) {
time = ""
}
if (time.isNotEmpty()) {
time = "$titleStart $time"
}
return time
}
private fun RemoteViews.setWidgetOnClickPendingIntent(
context: Context,
id: Int,
action: String,
) = this.apply {
setOnClickPendingIntent(id, PendingIntent
.getBroadcast(
context, 0, Intent().setAction(action),
PendingIntent.FLAG_IMMUTABLE
))
}
private fun updateAnimate(state: Int) {
Log.i("valueAnimator:value=", valueAnimator.toString())
valueAnimator.repeatCount = INFINITE
valueAnimator.duration = duration
valueAnimator.repeatMode = RESTART
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.addUpdateListener {
updateWave(context, it.animatedValue as Int)
}
Log.i("state::==", state.toString())
when (state) {
STATE_RECORDING, STATE_RECORDING_FROM_PAUSED -> {
if (valueAnimator.isPaused) {
valueAnimator.resume()
} else if (!valueAnimator.isRunning) {
valueAnimator.start()
}
}
STATE_IDLE, STATE_RECORD_PAUSED -> {
valueAnimator.removeAllUpdateListeners()
valueAnimator.pause()
isFirst = true
}
}
}
}
接下来运行结果:
同样,我们实现水波图的时候,不就缺少一个图像数组吗? 只需按照上面相同的步骤即可找到素材图片。
至此,大家都觉得这个实现不太给力。 这种效果是通过帧图片来实现的。 如果有能力,你可以通过代码写一波水波纹或者声音波纹之类的小部件。 当然,在做这种比较高端的操作之前,我们先研究一下如何实现定制。 当我们能够突破定制的时候,实现这种动画就没有问题了。 接下来我们探讨如何将小部件引入桌面。
5. 定制
.(id,) 自然出现。 了解API并多次使用的开发者应该能够想到(@)才是像素的真正载体,只是一张光栅画布,我们所有花哨的操作*终都会存储在它上面并设置到视图组件中。 那么我们先画一条线吧? 感受一下是否可行。
private fun drawCanvas(remoteViews: RemoteViews, index: Int) {
val width = context.resources.getDimensionPixelSize(R.dimen.widget_canvas_width)
val height = context.resources.getDimensionPixelSize(R.dimen.widget_canvas_height)
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint().apply {
this.color = Color.argb(115, 194, 108, 57)
this.strokeWidth = 2f
this.style = Paint.Style.STROKE
}
canvas.drawLine(0f, height/2f, width.toFloat(), height/2f, paint)
remoteViews.setImageViewBitmap(
R.id.widget_canvas, bitmap)
}
运行效果如下:
我们在这里找到突破口了吗? 只要能加载,定制不是问题。
1.定制雷达系列图
之前写过一些使用js的前端定制。 你可以看一下效果。 在这种情况下,我会给你一系列的雷达图。
如果您对定制不熟悉,可以阅读我之前的文章。 当然,亲自动手是唯一的方法。 知道了就知道,做完就忘记了。 我们用一个案例来看看一个自定义的简单API加上初中的简单数学计算能给我们带来什么。
1. 绘图前分析
坐标变换到屏幕中心带来的便利
绘制多条骨架线段
如何将实际数据映射到屏幕
连接填充完成
2.将坐标变换到屏幕上
canvas.translate(width / 2f, height / 2f)
canvas.scale(1f, -1f)
canvas.save()
3.绘制多条骨架线段
我们看到总共有三条骨架直线将屏幕分成六等份。 我们可以简单地求出这三个线段的方程,对吗? 我相信你能看懂初中数学。
Yx=-tan30*x
Yx=tan30*x
//右边的有个
val pathRight = Path()
val tan30 = kotlin.math.tan(Math.PI / 180 * 30)
val y1 = tan30 * (-width / 2)
val y2 = tan30 * width / 2
pathRight.moveTo(-width.toFloat() / 2, y1.toFloat())
pathRight.lineTo(width.toFloat() / 2, y2.toFloat())
canvas.drawPath(pathRight, paint)
//绘制中间一个
canvas.drawLine(0f, y1.toFloat() * 1.7f, 0f, -y1.toFloat() * 1.7f, paint)
//左边的一个
val pathLeft = Path()
pathLeft.moveTo(-width.toFloat() / 2, -y1.toFloat())
pathLeft.lineTo(width.toFloat() / 2, -y2.toFloat())
canvas.drawPath(pathLeft, paint)
paint.color = Color.argb(255, 66, 39, 39)
for (index in 0..10) {
canvas.drawCircle(0f, 0f, 50f * index, paint)
}
4. 如何将实际数据映射到屏幕上
同样,我们圆的半径可以看成是每个骨架坐标轴的长度,而我们的实际数据只是长度数据。 如何将长度数映射到各个不规则骨架坐标轴上? 当然,还是离不开简单的数学。 例如,我们有数字 250,两条白色虚线相交,如下所示。 我们实际的250代表的是从点到焦点部分的长度。 但如果我们需要在坐标系中定位,就需要找到(x,y)在坐标系中的虚拟坐标。 同样简单的初中数学,不难求出(x,y)=(,)。 如果仔细分析每个骨架坐标轴上的所有坐标都满足(x,y) = (,)。接下来我们去代码看看效果
paint.style = Paint.Style.FILL
paint.color = Color.argb(60, 154, 108, 57)
val arrData = arrayListOf(
arrayOf(300f, 200f, 300f, 300f, 266f, 133f),
arrayOf(200f, 245f, 300f, 201f, 220f, 200f),
arrayOf(130f, 295f, 180f, 151f, 220f, 120f),
arrayOf(220f, 235f, 200f, 199f, 200f, 130f),
arrayOf(110f, 135f, 300f, 199f, 150f, 220f),
arrayOf(150f, 235f, 100f, 300f, 50f, 110f),
arrayOf(100f, 40f, 80f, 70f, 36f, 23f)
)
for (index in 0 until arrData.size) {
val result = Path().apply {
moveTo(0f, arrData[index][0])
val random2 = arrData[index][1]
lineTo(random2, (random2 * tan30).toFloat())
val random4 = arrData[index][2]
lineTo(random4, -(random4 * tan30).toFloat())
val random5 = arrData[index][3]
lineTo(0f, -random5)
val random6 = arrData[index][4]
lineTo(-random6, -(random6 * tan30).toFloat())
val random7 = arrData[index][5]
lineTo(-random7, (random7 * tan30).toFloat())
close()
}
canvas.drawPath(result, paint)
}
paint.strokeWidth = 2f
paint.style = Paint.Style.STROKE
paint.color = Color.argb(35, 254, 108, 57)
5. 连接钩边
paint.strokeWidth = 2f
paint.style = Paint.Style.STROKE
paint.color = Color.argb(35, 254, 108, 57)
for (index in 0 until arrData.size) {
val result = Path().apply {
moveTo(0f, arrData[index][0])
val random2 = arrData[index][1]
lineTo(random2, (random2 * tan30).toFloat())
val random4 = arrData[index][2]
lineTo(random4, -(random4 * tan30).toFloat())
val random5 = arrData[index][3]
lineTo(0f, -random5)
val random6 = arrData[index][4]
lineTo(-random6, -(random6 * tan30).toFloat())
val random7 = arrData[index][5]
lineTo(-random7, (random7 * tan30).toFloat())
close()
}
canvas.drawPath(result, paint)
}
运行效果如下:
*终效果
此时我们就可以任意定制了。 那么,我们还需要使用帧动画来完成水波纹和音频抖动吗? 当然,为了还原更真实的水波纹和晃动动画,只能对帧动画进行粗略的移动。 稍后我们将实现如何自定义水波纹和声波动画。
应该需要一段时间。 *近比较忙,忙公司事务,只有周末才有时间写。 我记得QQ群里有人说过,我写的文章一定是工作不饱和才能写的文章。 其实周末我三四个小时就可以写一篇文章。 编程更多的是实践,没有捷径,多写,多读,少说,不要羞于问问题。 来吧朋友们。
之前写过一点,但没有写完。 这两篇文章绝对献给大家。
6.MIUI小部件
看了半天也没找到*难的。 我看到小米集团用了黑线图,可能需要做点什么。 发布其他图片并在此基础上设置本地动态应该没问题。 那么让我们做一些困难的事情吧。 当然,如果不好看,我只能说我的UI能力有限。 但困难一定不能丢失。 代码中有注释。 如果您不知道如何自定义它,请参阅上面的链接。
//1.cnavas坐标系的变换绘制
private fun canvasChangeSave(canvas: Canvas) {
val paint= Paint()
paint.color= Color.argb(55,111,111,111)
paint.strokeWidth=2f
paint.style= Paint.Style.STROKE
paint.strokeCap= Paint.Cap.ROUND
//x轴竞相
canvas.scale(1f,-1f)
//坐标系向下平移
canvas.translate(yMargerLeft, -height.toFloat()+xMargerBootom)
//绘制圆圈测试ok没问题删掉就行
//canvas.drawCircle(0f,0f,10f,paint
val yGridHeight = (height.toFloat()-xMargerBootom-canvasTop)/5
val xLength=width.toFloat()-yMargerLeft-canvasMargerRight
val path=Path()
path.lineTo(xLength,0f)
canvas.save()
//绘制线平行与X轴的
for(index in 0 until 5){
canvas.drawPath(path,paint)
canvas.translate(0f,yGridHeight)
}
canvas.restore()
//绘制折线
drawLine(yGridHeight, canvas, paint)
//绘制日期
drawXTextOfBootomLine(canvas)
//绘制y轴数值
drawYTextOfYLeft(canvas)
//绘制下面的文字框和文字,不是本节重点,所以为了快我就不分开讲解了
drawRectAndText(canvas)
//绘制顶部的文字
drawTopText(canvas)
}
private fun drawTopText(canvas: Canvas) {
val paint= Paint()
paint.color= Color.BLACK
paint.strokeWidth=5f
paint.style= Paint.Style.STROKE
paint.strokeCap= Paint.Cap.ROUND
paint.textSize=56f
paint.letterSpacing=0.5f
canvas.translate(-100f, (height-xMargerBootom-100f))
canvas.scale(1f,-1f)
canvas.drawText("近一年净值走势",0f,-50f,paint)
canvas.translate(0f,100f)
paint.letterSpacing=0f
paint.textSize=36f
paint.strokeWidth=2f
paint.color= Color.argb(150,111,111,111)
canvas.drawText("累计净值: 1。97744 单位净值:1。97773 日张丢福: -0。005%",0f,-80f,paint)
}
private fun drawRectAndText(canvas: Canvas) {
val paint= Paint()
paint.color= Color.argb(50,111,111,111)
paint.strokeWidth=5f
paint.style= Paint.Style.STROKE
paint.strokeCap= Paint.Cap.ROUND
paint.textSize=36f
canvas.save()
canvas.translate(-100f,-100f)
val topLine=Path()
topLine.moveTo(0f,0f)
topLine.lineTo(1790f,0f)
topLine.rLineTo(0f,-100f)
topLine.rLineTo(-1790f,0f)
topLine.close()
canvas.drawPath(topLine,paint)
val oneWidth=1790f/5
for (index in 0 until 5){
canvas.save()
canvas.scale(1f,-1f)
paint.color=Color.GRAY
canvas.drawText("${index*3}个月",40f,60f,paint)
canvas.restore()
canvas.translate(oneWidth,0f)
val onetopLine=Path()
onetopLine.moveTo(0f,0f)
onetopLine.lineTo(0f,-100f)
paint.color= Color.argb(50,111,111,111)
canvas.drawPath(onetopLine,paint)
}
canvas.restore()
}
private fun drawYTextOfYLeft(canvas: Canvas) {
val paint= Paint()
paint.color= Color.argb(150,111,111,111)
paint.strokeWidth=2f
paint.style= Paint.Style.STROKE
paint.strokeCap= Paint.Cap.ROUND
paint.textSize=30f
canvas.save()
canvas.scale(1f,-1f)
val yGridHeight = (height.toFloat()-xMargerBootom-canvasTop)/5
for (index in 0 until 5){
canvas.drawText("${10900+2900*index}",-100f,10f,paint)
canvas.translate(0f,-yGridHeight)
}
canvas.restore()
}
private fun drawXTextOfBootomLine(canvas: Canvas) {
val paint= Paint()
paint.color= Color.argb(150,111,111,111)
paint.strokeWidth=2f
paint.style= Paint.Style.STROKE
paint.strokeCap= Paint.Cap.ROUND
paint.textSize=30f
canvas.save()
canvas.scale(1f,-1f)
canvas.drawText("2020-02-25",0f,50f,paint)
canvas.drawText("2020-08-21",width/3f,50f,paint)
canvas.drawText("2021-02-25",1472f,50f,paint)
canvas.restore()
}
private fun drawLine(
yGridHeight: Float,
canvas: Canvas,
paint: Paint
) {
//绘制折线
val dataList: ArrayList = ArrayList()
dataList.add(PointF(20f, yGridHeight))
dataList.add(PointF(30f, yGridHeight - 70f))
dataList.add(PointF(60f, yGridHeight))
dataList.add(PointF(120f, yGridHeight - 90))
dataList.add(PointF(160f, 40f))
dataList.add(PointF(200f, 90f))
dataList.add(PointF(400f, yGridHeight + 30f))
dataList.add(PointF(500f, yGridHeight + 60f))
dataList.add(PointF(700f, yGridHeight * 2 + 90))
dataList.add(PointF(760f, yGridHeight * 2 + 10))
dataList.add(PointF(820f, yGridHeight * 2))
dataList.add(PointF(870f, yGridHeight * 2 + 134))
dataList.add(PointF(920f, yGridHeight * 2 + 54))
dataList.add(PointF(970f, yGridHeight * 2 - 111))
dataList.add(PointF(1170f, yGridHeight * 2 + 111))
dataList.add(PointF(1270f, yGridHeight * 2 ))
dataList.add(PointF(1370f, yGridHeight * 3 + 11))
dataList.add(PointF(1470f, yGridHeight * 3 + 21))
dataList.add(PointF(1570f, yGridHeight * 3 - 21))
dataList.add(PointF(1670f, yGridHeight * 4 - 21))
dataList.add(PointF(1690f, yGridHeight * 4 - 41))
dataList.add(PointF(1790f, yGridHeight * 4 + 71))
val fillPaint=Paint()
fillPaint.style=Paint.Style.FILL
val colorTop= intArrayOf(Color.argb(65,79,185,246),Color.WHITE)
fillPaint.shader=LinearGradient(0f,yGridHeight * 4 + 71,0f,0f,colorTop,null,Shader.TileMode.CLAMP)
//1.绘制折线渐变部分
val linePathGrident = Path()
linePathGrident.moveTo(20f, yGridHeight)
for (index in 0 until dataList.size - 1) {
linePathGrident.lineTo(dataList[index].x, dataList[index].y)
}
linePathGrident.lineTo(1690f, yGridHeight * 4 - 41)
linePathGrident.lineTo(1690f,0f)
linePathGrident.lineTo(20f,0f)
linePathGrident.close()
canvas.drawPath(linePathGrident, fillPaint)
//2.绘制折线
val linePath = Path()
linePath.moveTo(20f, yGridHeight)
for (index in 0 until dataList.size - 1) {
linePath.lineTo(dataList[index].x, dataList[index].y)
}
val tPath = Path()
tPath.addCircle(0f, 0f, 19f,Path.Direction.CCW)
val tPath2 = Path()
tPath2.addCircle(0f, 0f, 25f,Path.Direction.CCW)
paint.setShadowLayer(30f,30f,0f,Color.BLUE)
//PathDashPathEffect.Style.ROTATE
val pathDshEffect1 = PathDashPathEffect(tPath, 45f, 16f, PathDashPathEffect.Style.ROTATE)
val pathDshEffect2 = PathDashPathEffect(tPath2, 160f, 60f, PathDashPathEffect.Style.ROTATE)
paint.pathEffect = ComposePathEffect(pathDshEffect2, pathDshEffect1)
paint.color = Color.argb(255,209,103,58)
canvas.drawPath(linePath, paint)
}
看看效果如何? 为了对比,我把*终效果复制到了MIUI桌面上。 至于它有多好看我就不多说了。 这是产品和UI的责任……所以定制和动画完全可以达到你想要的效果。
MIUI、IOS等小部件更是主流、潮流。 我们不能把它拉下来,希望你也能制作出自己引以为豪的小部件,并与大家分享。 打破思维,动手动脑,提升认知,构建良好架构。 学习永无止境,不同的设计需要探索、创新、勇于尝试。
7. 新年快乐
新的一年里,祝大家身体健康,财源滚滚,生三胞胎。 我给你呼呼的祝福app开发,呼呼的甜蜜,呼呼的幸运。