Android Widget工作原理详解(一) 最全介绍

     

   转载请标明出处:http://blog.csdn.net/sk719887916/article/details/46853033 ;

      Widget是安卓的一应用程序组件,学名窗口小部件,它是微型应用程序视图, 可以嵌入到其他应用程序(如主屏幕)和接收数据定期更新。,可以使其他应用程序的插件被称为应用程序部件。用户可以通过添加窗口小部件来添加自己喜欢的APPwidget ,widget主要用于展现程序快捷入口,下面的屏幕快照展示了音乐应用程序的Widget。


                                    


 本文描述了如何使用应用程序部件发布应用程序提供者。创建您自己的喜欢的AppWidgetHost主机应用程序的小部件。


一 创建AppWidget组件


   1 AppWidgetProviderInfo 


      描述了应用程序的元数据部件,如应用程序部件的布局,更新频率,AppWidgetProvider类。这需要在XML中定义。


 2 AppWidgetProvider 


     定义允许的基本方法与应用程序编程接口部件,基于广播事件。通过它我们将会收到广播 ,用来更新应用程序的widget,用来进行启用,关闭,删除的操作。继承父类是一个BroadcastReceiver,拥有广播的一切特性,我们可以这么理解:AppWidgetProvider 是带有界面的广播。


3 视图布局 View layout

在创建时间。

怎么创建widget。

 A :在清单中申明widget部件

  首先,声明应用程序的AndroidManifest AppWidgetProvider类。xml文件。例如:

<receiver android:name="ExampleAppWidgetProvider" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data android:name="android.appwidget.provider"
               android:resource="@xml/example_appwidget_info" />
</receiver>

<receiver>需要Android:name属性,它指定了AppWidgetProvider的具体类。

< intent-filter >元素必须包含一个<action>元素与android:name属性。这个属性指定AppWidgetProvider接受系统的ACTION_APPWIDGET_UPDATE广播。这是唯一的广播,申明,您必须显式地声明。代表此类就是一个widget。AppWidgetManager 自动发送所有其他应用程序部件广播此注册的广播才能收到,也就是说我们必须要指定识别为widget的action,当然你需要这个AppWidgetProvider接收接她action,ni

 <Mata_data>元素 指定AppWidgetProviderInfo 资源和需要以下属性:

 android:name——指定Mata_data名称。使用android.appwidgetb必须确定AppWidgetProviderInfo描述符的数据。

 android:resource——指定AppWidgetProviderInfo资源XML。


二 添加 AppWidgetProviderInfo


      AppWidgetProviderInfo定义了应用程序的小部件的基本属性,如最小尺寸布局,其最初的布局资源,多久更新应用程序的小部件,和(可选)配置活动启动创建时间。定义AppWidgetProviderInfo在XML资源使用一个< appwidget-provider >标签,并将其保存在项目的res / XML /文件夹下。
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/preview"
    android:initialLayout="@layout/example_appwidget"
    android:configure="com.example.android.ExampleAppWidgetConfigure" 
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen|keyguard"
    android:initialKeyguardLayout="@layout/example_keyguard">
</appwidget-provider>

   < appwidget-provider >属性:介绍
       minWidth和minHeight属性指定了widget所占据的宽和高。主屏幕位置默认的应用程序的widget基于网格,其每个item有一个定义的高度和宽度。如果应用程序部件的最小宽度或高度值不匹配的屏幕的单元宽高,,然后widget的尺寸就会自动缩放到最适合的网格大小。

     注意:如果想让你的widget跨设备、移植widget的最小大小不应超过4 x 4单元。


    minResizeWidth和minResizeHeight属性指定widget的绝对最小大小。Android 3.1中引入此属性,用来指定应用程序的小部件的大小低于多少将字迹模糊的或无法使用。此属性允许用户自定义调整大小,但此widget可能小于默认小部件 由minWidth和minHeight属性定义的大小。。


    updatePeriodMillis:用来  设置widget更新时间,也就是会所从AppWidgetProvider通过调用onUpdate()回调方法更新数据的时间周期。实际的更新不能保证准时就能更新,为了节省电量和保护电池,规格建议一般一小时更新一次。开发者还可以让用户调整频率configuration-some。
   注意:如果设备时熄屏状态下 ,需要更新(如由updatePeriodMillis定义)的时候 ,设备就会自动唤醒,用来执行更新。如果每小时更新不超过一次,这可能不会导致电池寿命的重大问题。然而,如果您需要更新很频繁频繁 ,或者设备在灭屏下不需要了更新,我们可以给更新操作时设置一个警报提醒,,此时在灭屏状态下也不会执行更新操作,。为此,设置一个报警意图 让AppWidgetProvider接收, 使用alarmmanager。设置报警action 为ELAPSED_REALTIME或RTC,    这样到了一定的更新时间,此事件就会被停止。然后设置updatePeriodMillis为零(“0”)。


initialLayout 属性:   定义了Widget的XML布局。
   配置属性定义了活动推出当用户添加应用程序部件,为了他(她)来配置应用程序窗口小部件的属性。这是无需必须添加的。


previewImage  在Android 3.0中引入的。指定了用户第一次看见预览界面的widget是什么样子。如果不设置,默认为APP的启动图标。这个字段对应于android:previewImage属性在AndroidManifest <recevicer>元素。


autoAdvanceViewId 属性指定应用程序的视图ID应该auto-advanced部件子视图部件的主机。这是在Android 3.0中引入的。


resizeMode  在Android 3.1中引入的。指定规则的一个小部件可以调整大小。使用这个属性使widget 的resizeable-horizontally,垂直,或在两个轴。用户touch-hold小部件,以显示其调整处理,然后拖动水平和/或垂直布局网格处理改变大小。值resizeMode属性包括“水平”、“垂直”,“没有”。设置一个部件resizeable水平方向和垂直方向,
minResizeHeight 属性指定的最低高度(dps)小部件可以调整大小。大于minHeight 或者如果没有启用垂直调整(见resizeMode)。在Android 4.0中引入的。
minResizeWidth属性指定的最小宽度(dps)小部件可以调整大小。 大于minWidth或 如果没有启用水平调整(见resizeMode)。在Android 4.0中引入的。
widgetCategory属性 决定widget是否可以显示在主屏幕上, 锁屏也包括其中。这个属性的值包括“home_screen”和“power”键。显示一个小部件都需要确保它遵循窗口小部件类的设计指导方针。有关更多信息,请参见启用应用程序部件千篇一律。默认值是“home_screen”。在Android 4.2中引入的。
initialKeyguardLayout   在Android 4.2中引入, 用来定义锁屏应用小部件的布局。同样android:initialLayout,可指定一个布局资源 ,可以立即出现,直到应用程序部件初始化,能够更新布局。


     你必须为应用程序在项目的res /布局/目录下定义个xml文件 ,因为widget的布局需要的RemoteViews的支持。不能随便定义自定义view,支持的控件有:

支持的布局:

支持的控件:


三   AppWidgetProvider 

     此类是widget的控制核心,主要控制添加,删除,更新等。

    onUpdate()   widget更新时触发

   onDeleted(Context, int[] )  widget被删除是触发

  
    onEnabled(Context),widget可用时触发

   

    onDisabled(Context)  widget不可用时触发

   onReceive(Context, Intent)  收到指定的广播时触发


 指定某个widget创建以及更新可以重写onUpdate() ,通过遍历注册的appwidget的ID,创建一个RemoteViews来加载布局,最后调用updateAppWidget

来加载界面。  

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        final int N = appWidgetIds.length;

        // Perform this loop procedure for each App Widget that belongs to this provider
        for (int i=0; i<N; i++) {
            int appWidgetId = appWidgetIds[i];

            // Create an Intent to launch ExampleActivity
            Intent intent = new Intent(context, ExampleActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

            // Get the layout for the App Widget and attach an on-click listener
            // to the button
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
            views.setOnClickPendingIntent(R.id.button, pendingIntent);

            // Tell the AppWidgetManager to perform an update on the current app widget
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

  用于接收指定意图,处理相关需求,可以重写onRecrive(),列如我们收到一个toast的动作时,显示一条Toast

@Override
    public void onReceive(Context context, Intent intent) {
        AppWidgetManager mgr = AppWidgetManager.getInstance(context);
        if (intent.getAction().equals(TOAST_ACTION)) {
            int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
            int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
            Toast.makeText(context, "Touched view " + viewIndex, Toast.LENGTH_SHORT).show();
        }
        super.onReceive(context, intent);
    }


     AppWidgetProvider只是一个方便的类。如果你想接收应用程序部件直接广播,您可以实现自己的BroadcastReceiver或复写onReceive()的回调方法。意图需要关心如下:

ACTION_APPWIDGET_UPDATE //处理更新

ACTION_APPWIDGET_DELETED // 处理删除

ACTION_APPWIDGET_ENABLED //可用

ACTION_APPWIDGET_DISABLED//不可用

ACTION_APPWIDGET_OPTIONS_CHANGED // 配置改变


四 RemoteViewsService



1. RemoteViews


    顾名思义,它是一个远程视图。App Widget中的视图,都是通过RemoteViews转换表现的。 
    在RemoteViews的构造函数中,通过传入layout文件的id来获取 “layout文件对应的视图(RemoteViews)”;调用RemoteViews中的方法能对layout中的组件进行设置  widgetViews.setOnClickPendingIntent(R.id.widget_btn, calendarIntent);  来设ID对应的Button的点击响应事件)。


ps: 如果使自己的自定义的view显示在widget上,我们必须在这个类中加上我们自定义的全路径,前提是我们有权限修改rom.

    可以将RemoteViews看做是widget资源视图的所有集合工具管理者。


2 RemoteViewsService


RemoteViewsService子类提供了RemoteViewsFactory用于填充远程集合视图。

具体地说,需要执行以下步骤:

子类RemoteViewsService。

 RemoteViewsService是一个远程的服务适配器可以请求RemoteViews,管理RemoteViews的服务。 

   在你RemoteViewsService子类,包括一个实现RemoteViewsFactory接口的类。RemoteViewsFactory之间是一个适配器的接口远程集合视图(如列表视图,显示数据表格,等等)和底层数据视图。实现执行RemoteViews对象中每一项的数据集。这个接口是一个适配器。

    一般,当App Widget 中包含“GridView、ListView、StackView等”集合视图时,才需要使用RemoteViewsService来进行更新、管理。(集合视图是指GridView、ListView、StackView等包含子元素的视图) 
    RemoteViewsService更新“集合视图”的一般步骤是: 
(01) 通过setRemoteAdapter来设置 “RemoteViews对应RemoteViewsService”。 
(02) 之后在RemoteViewsService中,实现RemoteViewsFactory接口。然后,在RemoteViewsFactory接口中对“集合视图”的各个子项进行设置(“集合视图”的各个子项:例如,GridView的每一个格子都是一个子项;ListView中的每一列也是一个子项)。
public class StackWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}




3  RemoteViewsFactory


     RemoteViewsFactory接口提供了应用程序的小部件的数据项的集合。要做到这一点,它结合了你的应用程序部件条目XML布局文件的源数据。这个源的数据可以从一个数据库到一个简单的数组。在StackView小部件示例中,数据源是一个WidgetItems数组。RemoteViewsFactory作为适配器的将数据到设置到RemoteViews上

    最重要的两个方法你需要实现你的RemoteViewsFactory子类onCreate()和getViewAt()。

   系统调用onCreate()首次在创建Factory。在这里可以设初始化一些数据。例如,StackView小部件示例使用onCreate()来初始化一个WidgetItem对象数组。

    通过RemoteViewsService中的介绍,我们可以了解“RemoteViewsService是通过RemoteViewsFactory来具体管理layout中集合视图的”,即“RemoteViewsFactory管理集合视图的实施者”。 
    RemoteViewsFactory是RemoteViewsService中的一个接口。RemoteViewsFactory提供了一系列的方法管理“集合视图”中的每一项。例如: 
(01)RemoteViews getViewAt(int position) 
      通过getViewAt()来获取“集合视图”中的第position项的视图,视图是以RemoteViews的对象返回的。
public RemoteViews getViewAt(int position) {

    // Construct a remote views item based on the app widget item XML file, 
    // and set the text based on the position.
    RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
    rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);

    ...
    // Return the remote views object.
    return rv;
}

 
(02)int getCount() 
      通过getCount()来获取“集合视图”中所有子项的总数。  

class StackRemoteViewsFactory implements
RemoteViewsService.RemoteViewsFactory {
    private static final int mCount = 10;
    private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
    private Context mContext;
    private int mAppWidgetId;

    public StackRemoteViewsFactory(Context context, Intent intent) {
        mContext = context;
        mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
    }

    public void onCreate() {
        // In onCreate() you setup any connections / cursors to your data source. Heavy lifting,
        // for example downloading or creating content etc, should be deferred to onDataSetChanged()
        // or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
        for (int i = 0; i < mCount; i++) {
            mWidgetItems.add(new WidgetItem(i + "!"));
        }
        ...
    }
...

 五 widget工作原理

   当widget指定其具体的AppWidgetProvider,AppWidgetProvider通过创建RemoteViews来加载视图,其RemoteViews将会调用setRemoteViewsAdapter来设置内部适配器,此适配器也将会继续获取widget管理器调用updateAppWidget()方法,此方法有会用远程视图工厂(RemoteViewsFactroy)来初始化数据并调用其onDataSetChanged()来通知适配器更新数据,具体更新那个widget的界面,是通过其GetViewAt将界面更新后并返回,其详细流程图如下:



        Widget使用集合的特征是能够为用户图提供更好的更新数据的视图内容的方法。例如,考虑Android 3.0 Gmail应用部件,它为用户提供了一个快照的收件箱。为做到这一点,你需要能够触发RemoteViewsFactory和RemoteViews获取并显示新的数据。使用AppWidgetManager的notifyAppWidgetViewDataChanged()即可通知更新数据,这个调用的结果会继续回调到RemoteViewsFactory的 onDataSetChanged()方法,这里你可以去拿初始化和拿任何的数据。请注意,调用onDataSetChanged()方法可以更新数据。在调用它之前将必须保证先完成从RemoteViewsFactory获取元数据或视图的数据。此外,调用在getViewAt()方法。如果这个调用需要很长时间去加载视图(规定RemoteViewsFactory getLoadingView()方法)将会显示在相应的位置集合视图,直到它返回真正结果为止。

  本文出处:http://blog.csdn.net/sk719887916/article/details/46853033 欢迎大家阅读。

     

   转载请标明出处:http://blog.csdn.net/sk719887916/article/details/46853033 ;

      Widget是安卓的一应用程序组件,学名窗口小部件,它是微型应用程序视图, 可以嵌入到其他应用程序(如主屏幕)和接收数据定期更新。,可以使其他应用程序的插件被称为应用程序部件。用户可以通过添加窗口小部件来添加自己喜欢的APPwidget ,widget主要用于展现程序快捷入口,下面的屏幕快照展示了音乐应用程序的Widget。


                                    


 本文描述了如何使用应用程序部件发布应用程序提供者。创建您自己的喜欢的AppWidgetHost主机应用程序的小部件。


一 创建AppWidget组件


   1 AppWidgetProviderInfo 


      描述了应用程序的元数据部件,如应用程序部件的布局,更新频率,AppWidgetProvider类。这需要在XML中定义。


 2 AppWidgetProvider 


     定义允许的基本方法与应用程序编程接口部件,基于广播事件。通过它我们将会收到广播 ,用来更新应用程序的widget,用来进行启用,关闭,删除的操作。继承父类是一个BroadcastReceiver,拥有广播的一切特性,我们可以这么理解:AppWidgetProvider 是带有界面的广播。


3 视图布局 View layout

在创建时间。

怎么创建widget。

 A :在清单中申明widget部件

  首先,声明应用程序的AndroidManifest AppWidgetProvider类。xml文件。例如:

<receiver android:name="ExampleAppWidgetProvider" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data android:name="android.appwidget.provider"
               android:resource="@xml/example_appwidget_info" />
</receiver>

<receiver>需要Android:name属性,它指定了AppWidgetProvider的具体类。

< intent-filter >元素必须包含一个<action>元素与android:name属性。这个属性指定AppWidgetProvider接受系统的ACTION_APPWIDGET_UPDATE广播。这是唯一的广播,申明,您必须显式地声明。代表此类就是一个widget。AppWidgetManager 自动发送所有其他应用程序部件广播此注册的广播才能收到,也就是说我们必须要指定识别为widget的action,当然你需要这个AppWidgetProvider接收接她action,ni

 <Mata_data>元素 指定AppWidgetProviderInfo 资源和需要以下属性:

 android:name——指定Mata_data名称。使用android.appwidgetb必须确定AppWidgetProviderInfo描述符的数据。

 android:resource——指定AppWidgetProviderInfo资源XML。


二 添加 AppWidgetProviderInfo


      AppWidgetProviderInfo定义了应用程序的小部件的基本属性,如最小尺寸布局,其最初的布局资源,多久更新应用程序的小部件,和(可选)配置活动启动创建时间。定义AppWidgetProviderInfo在XML资源使用一个< appwidget-provider >标签,并将其保存在项目的res / XML /文件夹下。
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/preview"
    android:initialLayout="@layout/example_appwidget"
    android:configure="com.example.android.ExampleAppWidgetConfigure" 
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen|keyguard"
    android:initialKeyguardLayout="@layout/example_keyguard">
</appwidget-provider>

   < appwidget-provider >属性:介绍
       minWidth和minHeight属性指定了widget所占据的宽和高。主屏幕位置默认的应用程序的widget基于网格,其每个item有一个定义的高度和宽度。如果应用程序部件的最小宽度或高度值不匹配的屏幕的单元宽高,,然后widget的尺寸就会自动缩放到最适合的网格大小。

     注意:如果想让你的widget跨设备、移植widget的最小大小不应超过4 x 4单元。


    minResizeWidth和minResizeHeight属性指定widget的绝对最小大小。Android 3.1中引入此属性,用来指定应用程序的小部件的大小低于多少将字迹模糊的或无法使用。此属性允许用户自定义调整大小,但此widget可能小于默认小部件 由minWidth和minHeight属性定义的大小。。


    updatePeriodMillis:用来  设置widget更新时间,也就是会所从AppWidgetProvider通过调用onUpdate()回调方法更新数据的时间周期。实际的更新不能保证准时就能更新,为了节省电量和保护电池,规格建议一般一小时更新一次。开发者还可以让用户调整频率configuration-some。
   注意:如果设备时熄屏状态下 ,需要更新(如由updatePeriodMillis定义)的时候 ,设备就会自动唤醒,用来执行更新。如果每小时更新不超过一次,这可能不会导致电池寿命的重大问题。然而,如果您需要更新很频繁频繁 ,或者设备在灭屏下不需要了更新,我们可以给更新操作时设置一个警报提醒,,此时在灭屏状态下也不会执行更新操作,。为此,设置一个报警意图 让AppWidgetProvider接收, 使用alarmmanager。设置报警action 为ELAPSED_REALTIME或RTC,    这样到了一定的更新时间,此事件就会被停止。然后设置updatePeriodMillis为零(“0”)。


initialLayout 属性:   定义了Widget的XML布局。
   配置属性定义了活动推出当用户添加应用程序部件,为了他(她)来配置应用程序窗口小部件的属性。这是无需必须添加的。


previewImage  在Android 3.0中引入的。指定了用户第一次看见预览界面的widget是什么样子。如果不设置,默认为APP的启动图标。这个字段对应于android:previewImage属性在AndroidManifest <recevicer>元素。


autoAdvanceViewId 属性指定应用程序的视图ID应该auto-advanced部件子视图部件的主机。这是在Android 3.0中引入的。


resizeMode  在Android 3.1中引入的。指定规则的一个小部件可以调整大小。使用这个属性使widget 的resizeable-horizontally,垂直,或在两个轴。用户touch-hold小部件,以显示其调整处理,然后拖动水平和/或垂直布局网格处理改变大小。值resizeMode属性包括“水平”、“垂直”,“没有”。设置一个部件resizeable水平方向和垂直方向,
minResizeHeight 属性指定的最低高度(dps)小部件可以调整大小。大于minHeight 或者如果没有启用垂直调整(见resizeMode)。在Android 4.0中引入的。
minResizeWidth属性指定的最小宽度(dps)小部件可以调整大小。 大于minWidth或 如果没有启用水平调整(见resizeMode)。在Android 4.0中引入的。
widgetCategory属性 决定widget是否可以显示在主屏幕上, 锁屏也包括其中。这个属性的值包括“home_screen”和“power”键。显示一个小部件都需要确保它遵循窗口小部件类的设计指导方针。有关更多信息,请参见启用应用程序部件千篇一律。默认值是“home_screen”。在Android 4.2中引入的。
initialKeyguardLayout   在Android 4.2中引入, 用来定义锁屏应用小部件的布局。同样android:initialLayout,可指定一个布局资源 ,可以立即出现,直到应用程序部件初始化,能够更新布局。


     你必须为应用程序在项目的res /布局/目录下定义个xml文件 ,因为widget的布局需要的RemoteViews的支持。不能随便定义自定义view,支持的控件有:

支持的布局:

支持的控件:


三   AppWidgetProvider 

     此类是widget的控制核心,主要控制添加,删除,更新等。

    onUpdate()   widget更新时触发

   onDeleted(Context, int[] )  widget被删除是触发

  
    onEnabled(Context),widget可用时触发

   

    onDisabled(Context)  widget不可用时触发

   onReceive(Context, Intent)  收到指定的广播时触发


 指定某个widget创建以及更新可以重写onUpdate() ,通过遍历注册的appwidget的ID,创建一个RemoteViews来加载布局,最后调用updateAppWidget

来加载界面。  

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        final int N = appWidgetIds.length;

        // Perform this loop procedure for each App Widget that belongs to this provider
        for (int i=0; i<N; i++) {
            int appWidgetId = appWidgetIds[i];

            // Create an Intent to launch ExampleActivity
            Intent intent = new Intent(context, ExampleActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

            // Get the layout for the App Widget and attach an on-click listener
            // to the button
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
            views.setOnClickPendingIntent(R.id.button, pendingIntent);

            // Tell the AppWidgetManager to perform an update on the current app widget
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

  用于接收指定意图,处理相关需求,可以重写onRecrive(),列如我们收到一个toast的动作时,显示一条Toast

猜你喜欢

转载自blog.csdn.net/one_of_a_kind/article/details/75578920