`
xiaonao880516
  • 浏览: 58111 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

ICS多平台界面支持原理分析

 
阅读更多
1      简介(Introduction)
Android4.0 推出后,宣布可以同时支持手机,平板,和电视多个平台。

    我们做了如下实验:

在模拟器上用不同设置的android 虚拟设备(AVD) 运行,可得到如下不同的效果。

         创建虚拟设备:

       

        使用此AVD设置,启动模拟器后运行联系人应用,主界面效果图如下:

针对传统的手机设置:

针对此配置的虚拟设备,启动模拟器后运行联系人应用,主界面效果图如下:



可以明显的看到,针对较小的屏幕,android实现了完全不同的效果。在平板模式下,针对大屏幕,设计了左侧列表,右侧详细内容的界面。这样的设计充分利用了屏幕空间,让用户在一屏中看到更多的信息,避免了屏幕看起来空旷。同时也让用户可以直接进行多种操作,包括打电话,发短信等等。简化了用户的操作过程,提升了用户的操作感受。

而在电话屏幕尺寸下,针对较小屏幕的设计方案使得用户界面看起来干净清爽而不拥挤。同时也比较紧凑,同样提供了很好的用户体验。

更重要的是,这两套界面是由同一个应用运行出来的效果。也就是说,只要我们预先规划设计好我们的代码,根据不同的平台(平板、手机)特性,按照一定的方法,设计实现我们的软件产品,就可以实现维护一套而在多个平台实现不同效果,这样能够大大减轻我们的工作量,实现不同平台的快速移植和升级。



2       原理分析
Android 在设计之初,就考虑到支持在不同设备上运行的问题。为应用开发者提供了一套解决

方案。

2.1    基本概念
下面介绍一些Android应用开发中与不同设备展示相关的一些术语:

屏幕尺寸(Screen size)

    屏幕的物理尺寸。 根据屏幕的对角线长度确定。

简而言之,Android把屏幕尺寸分为四大类: 小( small),中等( normal),大(large), 和超大( extra large)。图一表示了屏幕尺寸的划分标准。

屏幕密度(Screen density)

    表示在一定的物理区域内能够显示的点的数目,通常用dpi(dot per inch)作为单位衡量。Dpi 的含义是一英寸之内有多少个点。密度越小的显示设备在固定区域内显示的点数越少。  

通常,Android跟据显示密度把屏幕也分为四种:低(low),中(medium),高(high),超高(extra high)。图一表示了屏幕密度的划分标准。

屏幕方向(Orientation)

    屏幕方向。包括横屏和竖屏。不同的设备有不同的缺省的屏幕方向。而且屏幕方向在运行时可以通过用户的旋转操作而更改。

分辨率(Resolution)

    一屏能显示的物理点数。在开发支持多屏显示的应用过程中,应用程序不应该直接根据屏幕的分辨率来设置。而应该更多考虑屏幕尺寸和密度。

密度无关像素(Density-independent pixel /dp)

    一种虚拟的像素单位,用此单位设计应用程序界面布局可以实现多屏幕支持效果。Dp表示的是一个准确的物理长度。它的大小等于显示密度为160dpi的一个像素的大小。如果在设计应用布局时以dp为单位。Android系统在渲染界面时会根据当前屏幕特显示密度来调整显示大小。有一个计算公式可以表明在不同显示密度屏幕上的像素显示大小为:

    dp = px * (dpi / 160)

    例如: 在显示密度为240dpi的屏幕上,一个dp的显示大小应该为1.5像素。

  

                  图一 android 系统对屏幕尺寸和密度的大概划分



2.2    原理分析
为了实现对多种分辨率和多种显示设备的支持,Android实现了一套机制,可以根据当前设

备的配置自动加载对应的资源文件以实现不同的效果。

2.2.1  提供可选资源
在Anroid 的应用程序开发中,会包含res目录,里面存储着所有应用程序需要的资源文

件,通常包括如下目录:

      \ drawable: 主要保存图标等资源文件。

      \ value: 主要保存字符串等资源文件。

      \ layout:主要保存布局等资源文件。

    针对不同的设备配置,可以通过如下方式定义适合设备的资源文件:

    \drawable-hdpi: 适合高清屏显示的图片资源。

    \layout-large: 适合大屏幕显示的布局文件。

       这样,在将应用程序装在到不同设备室,Android会根据不同的系统配置为应用程序加载对应的资源文件。下一节将描述android选择资源的基本方法。

2.2.2  选择可选资源
本节通过一个例子来说明在运行过程中Android如何选择适合的资源文件:

假定运行设备的配置如下:



Locale = en-GB

Screen orientation = port

Screen pixel density = hdpi

Touchscreen type = notouch

Primary text input method = 12key



而应用程序提供了如下资源文件目录:

drawable/

drawable-en/

drawable-fr-rCA/

drawable-en-port/

drawable-en-notouch-12key/

drawable-port-ldpi/

drawable-port-notouch-12key/



Android 将按照如下算法流程进行筛选:

         



具体步骤与结果如下所示:

1.       从目录列表中删除与设备配置相冲突的目录。

            drawable-fr-rCA/ 目录被排除, 因为当前区域设置的是en-GB.



drawable/

drawable-en/

drawable-fr-rCA/

drawable-en-port/

drawable-en-notouch-12key/

drawable-port-ldpi/

drawable-port-notouch-12key/

2.       按照下面链接中表二的次序,依次比较对应的目录。

sdk/android-sdk-linux_x86/docs/guide/topics/resources/providing-resources.html

3.       有某个目录包含表中所列出的限定条件吗?

1)      如果没有,返回2 继续比较。

2)      如果有,则转入4。

4.       排除不包含限定条件的目录项。在本文的例子中,

drawable/

drawable-en/

drawable-en-port/

drawable-en-notouch-12key/

drawable-port-ldpi/

drawable-port-notouch-12key/

标记为红色的三个目录被排除。因为所选择的语言是英语。

5.       重复2,3,4直到只剩下一个目录。在本例中,

drawable-en/

drawable-en-port/

drawable-en-notouch-12key/



最终选择了作为资源文件目录。



2.2.3  Android 3.2的扩展
在Android3.0 开始支持平板。开发平板应用时,需要吧布局相关文件加入res/layout-xlarge/

目录中。但是由于有些情况下,这种按照级别分类的情况并不能很好的支持对平板的支持,例如:对7寸屏和五寸屏的布局按照上面所述的分类标准都应该放置在res/layout-large/。但是,显而易见,这两款屏幕差别甚大,应该使用不同的布局,因此,Android 3.2以后提供了更加精准的支持。

其实现原理是:用户定义每一个布局时,需要声明他可以工作的屏幕的最小尺寸。这样就可以更准确的定义每个布局的工作环境。表示方法如下表所示:



屏幕配置

限定值

描述

smallestWidth

sw<N>dp



例如:

sw600dp

sw720dp

屏幕尺寸限制。描述屏幕的长和宽中最小值需要满足的限制条件。当屏幕发生旋转时,这个属性值不变。

Available screen width

w<N>dp



例如:

w720dp

w1024dp

屏幕宽度需要满足的最小尺寸。

Available screen height

h<N>dp



例如:

h720dp

h1024dp

etc.

屏幕高度需要满足的最小尺寸。

通过这三个变量的描述,可以更加精确的对布局进行限定。下一节将结合代码对如何使用此特性实现支持多平台的应用程序进行分析。





3      模块代码分析


3.1    联系人模块


导入联系人模块代码,用eclipse打开资源目录,看到列表如下:

参照前面的分析,布局文件主要有5个目录:

        分析源代码,当启动联系人第一屏时,运行了PeopleActivity.java。这个activity加载了people_activity.xml文件作为界面布局。而这个布局文件在layout-sw580dp-w1000dp和layout中都有定义。根据前文的分析和验证结果,layout中的布局文件应该是手机界面使用的,而layout-sw580dp-w1000dp中定义的布局文件是平板界面使用的。

        下面是对这一分析结果的验证:

(1)     修改layout 目录下的布局文件,people_activity.xml: 见下文,红色为后来添加的部分:

(2)    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"

(3)        android:id="@+id/list_container"

(4)        android:layout_width="match_parent"

(5)        android:layout_height="match_parent">

(6)       

(7)        <LinearLayout

(8)                android:id="@+id/browse_view"

(9)                android:layout_width="match_parent"

(10)            android:layout_height="match_parent"

(11)            android:orientation="vertical"

(12)            android:background="@drawable/list_background_holo">

(13)          

(14)    <!-- add a text view for test if load this layout file on default phone mode -->

(15)    <TextView

(16)        android:id="@+id/default_text"

(17)        android:text="@string/layout_choose_test"

(18)        android:layout_height="20dp"

(19)        android:layout_width="match_parent"

(20)        />

(21)  

(22)    <Button

(23)        android:id="@+id/change_text_view"

(24)        android:layout_width="match_parent"

(25)        android:layout_height="wrap_content"

(26)        android:text="@string/layout_choose_test" />

(27)

(28)    <!--

(29)        ViewPager for swiping between tabs.  We put StrequentContactListFragment,

(30)        DefaultContactBrowseListFragment and GroupBrowseListFragment at runtime.

(31)

(32)        (Adding them directly as the children of this view is not recommended.  ViewPager should

(33)        be treated like a ListView, which doesn't expect children to be added from the layout.)

(34)    -->

(35)    <android.support.v4.view.ViewPager

(36)        android:id="@+id/tab_pager"

(37)        android:layout_height="match_parent"

(38)        android:layout_width="match_parent"

(39)        />

(40)

(41)

(42)

(43)    <FrameLayout

(44)        android:id="@+id/contacts_unavailable_view"

(45)        android:layout_width="match_parent"

(46)        android:layout_height="match_parent"

(47)        android:visibility="gone">

(48)        <FrameLayout

(49)            android:id="@+id/contacts_unavailable_container"

(50)            android:layout_height="match_parent"

(51)            android:layout_width="match_parent" />

(52)    </FrameLayout>

(53)      </LinearLayout>

(54)</FrameLayout>

运行结果如下图:

     



     显然,运行结果验证了我们的分析。

     同样验证layout-sw580dp-w1000dp下的people_activity.xml:

     布局:

     <FrameLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:ex="http://schemas.android.com/apk/res/com.android.contacts"

    android:layout_width="match_parent"

    android:layout_height="match_parent">



    <com.android.contacts.widget.InterpolatingLayout

        android:id="@+id/main_view"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:splitMotionEvents="true">



        <LinearLayout

            android:id="@+id/browse_view"

            android:layout_width="wrap_content"

            android:layout_height="match_parent"

            android:orientation="vertical"

            android:minWidth="100dip"

            ex:layout_narrowParentWidth="1000dip"

            ex:layout_narrowWidth="276dip"

            ex:layout_wideParentWidth="1280dip"

            ex:layout_wideWidth="376dip"

            android:background="@drawable/list_background_holo"

            android:visibility="gone">

          

            <TextView

                android:id="@+id/sw580dp_w1000dp"

                android:text="@string/layout_choose_test_sw580dp_w1000dp"

                android:layout_height="20dp"

                android:layout_width="match_parent"

            />

          

            <Button

              android:id="@+id/change_text_view"

              android:layout_width="match_parent"

              android:layout_height="wrap_content"

              android:text="@string/layout_choose_test_sw580dp_w1000dp" />

          



            <!-- All -->

            <fragment

                android:id="@+id/all_fragment"

                class="com.android.contacts.list.DefaultContactBrowseListFragment"

                android:layout_height="0dip"

                android:layout_width="match_parent"

                android:layout_weight="1" />



            <!-- Groups -->

            <fragment

                android:id="@+id/groups_fragment"

                class="com.android.contacts.group.GroupBrowseListFragment"

                android:layout_height="match_parent"

                android:layout_width="match_parent" />

        </LinearLayout>



        <view

            class="com.android.contacts.widget.TransitionAnimationView"

            android:id="@+id/details_view"

            android:layout_width="match_parent"

            android:layout_height="match_parent"

            ex:layout_narrowParentWidth="800dip"

            ex:layout_narrowMarginLeft="0dip"

            ex:layout_narrowMarginRight="0dip"

            ex:layout_wideParentWidth="1280dip"

            ex:layout_wideMarginLeft="0dip"

            ex:layout_wideMarginRight="0dip"

            ex:clipMarginLeft="0dip"

            ex:clipMarginTop="3dip"

            ex:clipMarginRight="3dip"

            ex:clipMarginBottom="9dip"

            ex:enterAnimation="@android:animator/fade_in"

            ex:exitAnimation="@android:animator/fade_out"

            ex:animationDuration="200"

            android:visibility="gone">



            <!-- This layout includes all possible views needed for a contact detail page -->

            <include

                android:id="@+id/contact_detail_container"

                layout="@layout/contact_detail_container"

                android:layout_width="match_parent"

                android:layout_height="match_parent"/>



            <!-- This invisible worker fragment loads the contact's details -->

            <fragment

                android:id="@+id/contact_detail_loader_fragment"

                class="com.android.contacts.detail.ContactLoaderFragment"

                android:layout_height="0dip"

                android:layout_width="0dip"

                android:visibility="gone"/>



            <!-- This is the group detail page -->

            <fragment

                android:id="@+id/group_detail_fragment"

                class="com.android.contacts.group.GroupDetailFragment"

                android:layout_width="match_parent"

                android:layout_height="match_parent"

                android:visibility="gone" />

        </view>



        <view

            class="com.android.contacts.widget.TransitionAnimationView"

            android:id="@+id/favorites_view"

            android:layout_width="match_parent"

            android:layout_height="match_parent"

            ex:clipMarginLeft="0dip"

            ex:clipMarginTop="3dip"

            ex:clipMarginRight="3dip"

            ex:clipMarginBottom="9dip"

            ex:enterAnimation="@android:animator/fade_in"

            ex:exitAnimation="@android:animator/fade_out"

            ex:animationDuration="200">



            <LinearLayout

                android:layout_width="match_parent"

                android:layout_height="match_parent"

                android:background="@drawable/list_background_holo">



                <!-- Starred -->

                <FrameLayout

                    android:layout_width="0dip"

                    android:layout_height="match_parent"

                    android:layout_weight="7"

                    android:background="@drawable/panel_favorites_holo_light">



                    <fragment

                        android:id="@+id/favorites_fragment"

                        class="com.android.contacts.list.ContactTileListFragment"

                        android:layout_height="match_parent"

                        android:layout_width="match_parent"

                        android:layout_marginTop="32dip"

                        android:layout_marginRight="32dip"

                        android:layout_marginLeft="32dip"/>



                </FrameLayout>



                <!-- Most Frequent -->

                <fragment

                    android:id="@+id/frequent_fragment"

                    class="com.android.contacts.list.ContactTileListFragment"

                    android:layout_width="0dip"

                    android:layout_height="match_parent"

                    android:layout_weight="3"

                    android:layout_marginTop="32dip"

                    android:layout_marginRight="16dip"/>



            </LinearLayout>

        </view>



    </com.android.contacts.widget.InterpolatingLayout>



    <com.android.contacts.widget.InterpolatingLayout

        android:id="@+id/contacts_unavailable_view"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:visibility="gone">



        <FrameLayout

            android:id="@+id/contacts_unavailable_container"

            android:layout_height="match_parent"

            android:layout_width="match_parent"

            ex:layout_narrowParentWidth="800dip"

            ex:layout_narrowMarginLeft="80dip"

            ex:layout_narrowMarginRight="80dip"

            ex:layout_wideParentWidth="1280dip"

            ex:layout_wideMarginLeft="200dip"

            ex:layout_wideMarginRight="200dip"

            android:paddingBottom="20dip" />



    </com.android.contacts.widget.InterpolatingLayout>

</FrameLayout>

     用T6的虚拟设备运行新应用,结果如下图所示:



同样和我们的分析结果一致。

关于界面布局的分析和验证结果是一致的,说明,android是根据当前的机器配置,在不同的资源文件目录下选择不同的资源进行加载,从而实现了不同配置下的不同布局。



另外一个问题,由于不同的界面布局导致代码控制逻辑的不同该如何处理呢?

分析联系人代码,发现有如下的控制逻辑:



onCreate -> createViewsAndFragments() {

    if (phoneCapabilityTest.isUsingTwoPanes())

{

    }

    Else

{

}

}



分析phoneCapabilityTest.isUsingTwoPanes()实现,

/* if we are using two-pane layouts ("tablet mode"), false if we are using single views

     * ("phone mode")

     * */

   public static boolean isUsingTwoPanes(Context context) {

return context.getResources().getBoolean(R.bool.config_use_two_panes);

}

显然,从代码注释看,此函数是用于判断当前模式,属于平板还是电话。同样添加代码进行验证。在前面的布局文件中,除了添加一个文本显示控件,还添加了一个按钮。在createViewsAndFragments中添加如下代码:

mTestButton.setOnClickListener(new OnClickListener() {

public void onClick(View v) {

           if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) {

               Log.d("","Click tablet text view");

               mTestTextView = (TextView)findViewById(R.id.sw580dp_w1000dp);                           mTestTextView.setText("Click tablet text view");

           }

           else

           {

                Log.d("","Click tablet phone view");

                mTestTextView = (TextView)findViewById(R.id.default_text);                               mTestTextView.setText("Click phone text view");

            }

       }

});



编译运行,测试结果如下:

首先在手机模式下:

显然,和分析结果一致。

平板测试结果如下:

同样和我们的分析结果一致。

因此,在联系人代码中就是使用isUsingTwoPanes来确定当前的模式。



进一步分析,为什么这个函数就可以判断这两种模式呢?

    public static boolean isUsingTwoPanes(Context context) {

return context.getResources().getBoolean(R.bool.config_use_two_panes);

}

实际上这个函数读取了资源中的一个配置变量config_use_two_panes。它在不同的资源文件中有不同的定义:

res/values-sw580dp/donottranslate_config.xml:  

<bool name="config_use_two_panes">true</bool>

res/values/donottranslate_config.xml:    <bool name="config_use_two_panes">false</bool>

再结合上面对资源目录命名规则的分析,可以清楚的知道,在平板模式下会读取 res/values-sw580dp/donottranslate_config.xml 而电话模式下读取 res/values/donottranslate_config.xml,从而实现了不同的显示模式。

3.2    电话模块
分析电话模块的资源文件目录没有区别手机和平板。因此。电话模块的手机和平板上运行效果是一致的。

经过运行测试也和分析结果一致。



3.3    短信模块
分析短信模块的资源文件目录,可以看到,没有区别手机和平板。因此。短信模块的手机和平板上运行效果是一致的。

经过运行测试也和分析结果一致。

4      如何设计自己的应用程序


结合上面的原理分析以及源码分析,我们如果要实现类似功能,需要做到如下几步:

1.    在设计产品之初,需要根据不同产品给出不同界面定义。此处需要考虑不同界面的功能一致性,以及界面之间的可重用性。

2.    针对产品定义,设计不同的界面布局。通过放置在不同的资源文件目录中,实现应用程序的动态加载。

3.    在应用代码中要针对不同的界面模式实现不同的控制逻辑。
分享到:
评论

相关推荐

    SZ.691.0A1.V2.rar_V2 _ics 文档

    ICS,即集成通信系统,是一种高度集成和智能化的通信解决方案,它将多种通信方式如无线电、卫星通信、电话、数据传输等整合在一个统一的平台上,实现多通道、多模式的交互。这种系统设计的目标是提高通信效率,降低...

    android 4.0 samples

    5. **碎片(Fragment)**:碎片是Android 4.0引入的新概念,用于支持多屏幕布局和更灵活的用户界面设计。示例会展示如何在不同屏幕尺寸和方向下使用和管理碎片。 6. **同步适配器(Sync Adapter)**:ICS提供了同步...

    android-4.0 源码

    Android 4.0,也被称为Ice Cream Sandwich (ICS),是Google在2011年推出的一个重要的Android操作系统版本。这个版本引入了许多新功能和改进,为开发者提供了更强大的平台,同时也提升了用户体验。本文将深入探讨...

    android sdk 4.0.3源碼

    4. **多任务支持**:Android 4.0.3增强了多任务处理能力,用户可以同时查看并切换多个应用程序。开发者可以通过源码学习如何创建支持多任务的应用。 5. **硬件加速**:ICS开始广泛使用硬件加速,提高图形渲染速度。...

    android-sdk-sources-android-14.rar

    1. UI 设计改进:ICS 引入了全新的 Holo 风格界面,提供了一致且现代的用户界面设计,增强了触控体验。 2. 同步与数据管理:ICS 提升了同步框架,使得应用程序能够更有效地处理数据同步,同时增加了账户管理的灵活性...

    android4.0 Browser 浏览器 源码

    Android 4.0是对Android系统的一次重大更新,引入了诸多新特性和改进,包括全新的用户界面、更好的多任务处理以及对更大屏幕设备的支持。Browser应用程序作为系统核心组件之一,负责渲染网页和提供交互体验。 ...

    Android 4.0 Eclipse 源码

    1. 统一的用户界面:Android 4.0设计了一个全新的、统一的UI,适用于手机和平板设备,提供了更好的多任务处理和手势支持。 2. 更强的通知系统:ICS的通知栏变得更加智能,允许用户直接在通知中进行操作,而无需进入...

    android4.0sdk源码

    这一版本引入了诸多新特性,如全新的用户界面设计、多任务处理优化、人脸解锁等,源码分析对于理解这些功能如何实现至关重要。 二、源码结构 Android源码主要分为以下几个部分: 1. System Core:包含Android系统的...

    机器人的动作编程与软件开发

    - **ICS3.5标准**:舵机采用的ICS3.5标准是一个更高级别的通信标准,它包含了更多的附加功能和扩展功能,能够满足更高层次的控制需求。 ##### 2. 图形化界面编排机器人的动作 机器人通过图形化界面编排动作是一种...

    android4.0-launcher可运行源码

    它使用了多个自定义视图,如GridView、DragLayer和Workspace,来构建用户界面。 2. **主要组件解析**: - **DragLayer**:DragLayer是所有拖放操作的基础,负责处理触摸事件和对象的移动。 - **Workspace**:这是...

    android 4.0 sdk android-14_r01.zip

    这个版本对用户界面进行了大幅度改进,增强了多任务处理能力,并引入了许多新特性,以提高用户体验和开发者的工作效率。在"android-14_r01.zip"这个压缩包中,包含了Android SDK针对4.0版本的所有核心组件和工具,供...

    Android4.0源码

    Android 4.0, 又称Ice Cream Sandwich (ICS),是Google于2011年发布的一个重大版本更新。该版本旨在统一手机和平板电脑的操作体验,并引入了一系列重要的功能改进和技术革新。 ### 源码的重要性 #### 学习价值 - *...

    Android 4.0 网络编程详解 王家林.pdf

    Android 4.0在性能优化、用户界面改进、网络功能增强等方面都有所提升,尤其是网络编程方面为开发者提供了更多便捷的API。 Android 4.0的网络编程主要涉及以下几个方面: 1. 网络权限管理:Android平台对网络访问...

    Android 4.0 源码

    Android 4.0,也被称为Ice Cream Sandwich (ICS),是Google推出的一个重要的Android操作系统版本,为移动设备带来了诸多新特性和改进。这个源码包含了Android系统的核心组件、库、服务以及用户界面等所有组成部分,...

    android4.0源码下载地址

    Android 4.0,也被称为Ice Cream Sandwich (ICS),是Google在2011年推出的一个重要的Android操作系统版本。这个版本引入了许多新功能和改进,为开发者提供了更强大的平台,同时也提升了用户体验。对于想要深入理解...

    安卓动画效果相关-ActivityOptionsICSactivity的各种切换动画.rar

    通过分析和修改这些代码,开发者可以深入理解Android动画的工作原理,掌握如何结合生命周期和视图状态来控制动画的播放,以及如何优化动画性能。 7. **注意事项**: 在实际应用中,需要注意动画的性能影响,避免...

    androd4.0源码

    Android 4.0,也被称为Ice Cream Sandwich (ICS),是Google推出的一个重要的Android操作系统版本,对用户界面进行了重大改进,并增强了设备性能。源码对于开发者来说是宝贵的资源,它允许深入理解系统的运作机制,...

    ChooseYourOwnAdventure:用Java创建的冒险游戏

    它允许开发者编写一次代码,然后在多个操作系统上运行,这使得Java成为开发跨平台游戏的理想选择。在这个冒险游戏中,Java的面向对象特性将被充分利用,如定义类、对象以及它们之间的交互。 游戏的核心机制是“选择...

Global site tag (gtag.js) - Google Analytics