实例波的Android备忘录

作者: 实例波 | 来源:发表于2017-10-21 16:23 被阅读382次

    前言

    新开一篇占个位置,记录一些零碎的知识点,有些细小的点没法单独写一篇文章,就记在这里面。不定期更新。

    1. 两个TextView,一左一右排成一排,右边的保持完整显示,并紧贴着左边,左边的如果过长,则显示省略号。

    这个需求描述起来可能有点说不清楚,我们看一下图就明白了: 正常显示.png
    左边的TextView过长.png

    一开始以为很简单,实现起来还是费了一番功夫去调试,不多说,直接上代码:

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:orientation="horizontal">
    
                    <TextView
                        android:id="@+id/tv_nick_name"
                        android:layout_height="wrap_content"
                        android:layout_width="0dp"
                        android:layout_weight="1"
                        android:ellipsize="end"
                        android:maxLines="1"
                        android:textSize="14dp" />
    
                    <TextView
                        android:id="@+id/tv_next_trip"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:maxLines="1"
                        android:text="@string/where_is_your_next_trip"
                        android:textSize="14dp" />
    
                </LinearLayout>
    

    在使用一段时间后,发现这种方法存在bug,在重复设置name值时,可能会出现name明明很短却展示了省略号,或者name后面多了好几个空格这样的问题,猜想可能在布局时出了问题,于是在设置name后,请求重新布局,问题得到解决,代码如下:

        tvNickName.setText(nickName);
        tvNickName.requestLayout();  // 请求重新布局
    

    2. RecyclerView移动指定位置item到顶部

    需求的场景是:在聊天页面,用户可以下拉加载更多消息,这个时候把请求到的新消息插入到头部,调用adapter.notifyDataSetChanged()就会把最顶部的新消息展示出来,而我想要的,是保持我刷新之前的item位于顶部,并露出一点点新的消息,所以我需要将指定位置的item移动到顶部。还是先看一下实际效果吧:

    刷新前.png
    刷新后.png 对于这样的需求,ListView已经为我们提供setSelectionFromTop()方法,但是RecyclerView好像并没有提供这样的方法,只有一个scrollToPosition()方法,这个方法只会让指定位置的item出现在屏幕内,却不保证所处的位置,甚至如果指定的元素已经可见,它连动都不会动一下...
    于是我在网上查了一下,大部分的答案都指向一种方法,先用scrollToPosition()方法让指定元素出现在屏幕内,然后再通过计算并且用scrollBy()方法进行二次调整,使用起来麻烦不说,或多或少还有些问题。于是我就在想有没有更加简便的实现方式,点进scrollToPosition()的源码里,我发现了一些情况:
    /**
         * Convenience method to scroll to a certain position.
         *
         * RecyclerView does not implement scrolling logic, rather forwards the call to
         * {@link android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)}
         * @param position Scroll to this adapter position
         * @see android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)
         */
        public void scrollToPosition(int position) {
            if (mLayoutFrozen) {
                return;
            }
            stopScroll();
            if (mLayout == null) {
                Log.e(TAG, "Cannot scroll to position a LayoutManager set. " +
                        "Call setLayoutManager with a non-null argument.");
                return;
            }
            mLayout.scrollToPosition(position);
            awakenScrollBars();
        }
    

    感情RecyclerView并没有实现这个方法啊,而是使用了LayoutManager里的scrollToPosition()方法,看到这里,我心里仿佛又有了点儿b数,点开LinearLayoutManager的源码,果然发现了我想要的东西:

        /**
         * Scroll to the specified adapter position with the given offset from resolved layout
         * start. Resolved layout start depends on {@link #getReverseLayout()},
         * {@link ViewCompat#getLayoutDirection(android.view.View)} and {@link #getStackFromEnd()}.
         * <p>
         * For example, if layout is {@link #VERTICAL} and {@link #getStackFromEnd()} is true, calling
         * <code>scrollToPositionWithOffset(10, 20)</code> will layout such that
         * <code>item[10]</code>'s bottom is 20 pixels above the RecyclerView's bottom.
         * <p>
         * Note that scroll position change will not be reflected until the next layout call.
         * <p>
         * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}.
         *
         * @param position Index (starting at 0) of the reference item.
         * @param offset   The distance (in pixels) between the start edge of the item view and
         *                 start edge of the RecyclerView.
         * @see #setReverseLayout(boolean)
         * @see #scrollToPosition(int)
         */
        public void scrollToPositionWithOffset(int position, int offset) {
            mPendingScrollPosition = position;
            mPendingScrollPositionOffset = offset;
            if (mPendingSavedState != null) {
                mPendingSavedState.invalidateAnchor();
            }
            requestLayout();
        }
    

    scrollToPositionWithOffset(int position, int offset),使用这个方法就可以实现想要的效果。先拿到LinearLayoutManager,再调用该方法即可:

        LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerview.getLayoutManager();
        layoutManager.scrollToPositionWithOffset(position, offset);
    

    后来我又想到其实还可以利用RecyclerView倒序列表来实现同样的效果,不过这个知识点记录下来以后还是能派上用场的。

    3. dp转pixel的三个方法间的区别

    以前需要dp转pixel的时候都是直接使用现成的工具类,今天偶然看到有三个系统方法,专门用来转换成pixel的,它们是分别是:getDimension() 、 getDimensionPixelSize() 和 getDimensionPixelOffset。它们的区别在哪呢?我们来看一下输出:

        getDimension: 44.625
        getDimensionPixelSize: 45
        getDimensionPixelOffset: 44
    

    同样的dp值,转换出来的pixel值却有所不同。根据得到的三个值,我们可以猜想它们分别是:原始的float值、四舍五入的int值、强制转换的int值。查看官方文档后,也验证了我的猜想。另附上使用方法:

        getResources().getDimension(R.dimen.activity_vertical_margin);
        getResources().getDimensionPixelSize(R.dimen.activity_vertical_margin);
        getResources().getDimensionPixelOffset(R.dimen.activity_vertical_margin);
    

    4. inflate方法参数的含义

    inflate方法有两参数和三参数的重载,我们先来看看两参数的方法:

        public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
            return inflate(parser, root, root != null);
        }
    

    两参数的方法其实就是 根据root是否为空,来决定第三个参数,并且调用三参数的方法,所以我们只需要弄明白三参数的方法即可:

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
    

    三参数的方法根据第2、3个参数传值的不同,可以分为以下三种情况:

    • root不为null,attachToRoot为true
      这种情况下会将resource指定的布局添加到root中,添加的过程中resource所指定的的布局的根节点的各个属性都是有效的,示例:
    LayoutInflater.from(this).inflate(R.layout.view_room_amenity_icon, amenitiesContainer, true);
    

    只需要这样就可以把view_room_amenity_icon布局添加到amenitiesContainer中,而不再需要调用amenitiesContainer.addView(view)了。

    • root不为null,而attachToRoot为false
      这种情况下不会将resource所指定的布局添加到root中,但是既然我们不添加到root中,我们还传一个root参数干嘛,直接传null不就行了吗?其实这里是有说道的,我们在开发的过程中给控件指定了layout_width和layout_height属性,这些属性表示一个控件在容器中的大小,就是说这个控件必须在容器中,这个属性才有意义。这就意味着如果我们直接将resource指定的布局加载进来而不给它指定一个父布局,那么它的根节点的layout_width和layout_height属性将会失效(因为这个时候它不处于任何容器中,那么它的根节点的宽高自然会失效)。如果我们想让resource指定的布局的根节点有效,又不想让其处于某一个容器中,那我们就可以设置root不为null,而attachToRoot为false。示例:
    LinearLayout item = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.view_room_amenity_icon, amenitiesContainer, false);
    ......
    amenitiesContainer.addView(item);
    

    我们可以在对item进行一些处理(如设置内部的图片、文字等)后,再将它通过addView方法添加到amenitiesContainer中。

    • root为null
      当root为null时,attachToRoot将失去作用,设置任何值都没有意义。当root为null表示我们不需要将resource所指定的布局添加到任何容器中,同时也表示没有任何容器来来协助resource所指定布局的根节点生成布局参数。示例:
    LinearLayout item = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.view_room_amenity_icon, amenitiesContainer, false);
    amenitiesContainer.addView(item);
    

    item添加到amenitiesContainer中,item的根节点的布局参数将会失效。

    我们最后再看看inflate方法的返回值,查阅官方文档后得知:当提供了root(root不为空)且attachToRoot为true时,返回root,否则返回resource所指定的布局。

    5. 控件 focusable 和 focusableInTouchMode 属性的区别

    大多数控件都可以获得焦点,如果focusable属性值为true,表示可以通过键盘(虚拟键盘或者物理键盘)或者轨迹球将焦点移动到当前控件上,如果该属性值为false,则无法将焦点移动到当前控件。

    只要我们用手触摸手机屏幕,便进入了触摸模式,即TouchMode。在默认情况下,触摸一个控件虽然可以触发该控件的单击事件,但无法使控件处在焦点状态。而设置focusableInTouchMode可以使控件通过触摸获取焦点。将focusableInTouchMode属性值设为true,当触摸某个控件时,会先将焦点移动到被触摸的控件上,然后需要再次触摸该控件才会响应单击事件。但是注意,我们需要将focusable属性值设为true,当前控件才可能获得焦点,否则当前控件无论使用何种方式都无法获得焦点。默认情况下,Button、TextView的focusableInTouchMode属性都为false,而EditText的focusableInTouchMode属性为true,因为EditText需要通过触摸获取焦点,而另外两个控件则不需要。

    • focusable为true,不会影响 focusableInTouchMode,如果focusableInTouchMode为false,在TouchMode状态下,依旧无法获取焦点
    • 如果focusable为false,一定会使focusableInTouchMode为false

    相对的

    • focusableInTouchMode为false,不会影响focusable
    • 如果focusableInTouchMode为true,一定会使focusable为true

    (已亲自验证)

    6. git fetch 和 git pull 的区别

    git pull 这条指令的内部实现就是把远程仓库使用 git fetch 取下来以后再进行 merge 操作。

    7.EditText 隐藏、显示密码

    在UI验收的时候,设计给我提了个bug,说密码输入框显示和隐藏状态下提示语的字体不一样,我试了一下确实有这个问题,但是只在英文状态下会出现:

    隐藏状态.png 显示状态.png 可以看到确实一个长一个短,很奇怪,因为我并没有调整字体和字号,而且中文状态下却没有问题… 我采用的方法是改变输入框的InputType:
    //显示密码
    etPassword.setInputType(InputType.TYPE_CLASS_TEXT);
    
    //隐藏密码
    etPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
    

    后来在网上查了一下,原来这种方式会将字体设置为等宽字体(猜测是出于安全考虑,无法通过密码的宽度来缩小破解范围),而我们的汉字又恰恰是等宽字体,所以只有英文出现了这个问题。努力了一番,没有太好的办法解决这个问题,索性换了一种实现方式,终于解决了这个问题。

    正确而完美的实现方式
    先在xml中设置EditText的属性:

    android:inputType="textPassword"
    

    再通过代码改变显示隐藏状态:

    //显示密码
    etPassword.setTransformationMethod(HideReturnsTransformationMethod.getInstance()); 
    
    //隐藏密码
    etPassword.setTransformationMethod(PasswordTransformationMethod.getInstance());  
    

    每次改变后,光标会跑到最前面,我们需要手动将光标置于内容的末端:

    //将光标置于末端
    etPassword.setSelection(etPassword.getText().length());  
    

    8. clipToPadding和clipChildren的使用

    clipToPadding:
    Defines whether the ViewGroup will clip its children and resize (but not clip) any EdgeEffect to its padding, if padding is not zero. This property is set to true by default.

    大概翻译一下:当padding不为0时,这个属性决定了ViewGroup是否裁剪自己padding内的子View。默认值为true,也就是默认会裁剪自己padding内的子View。

    也可以理解为:ViewGroup的绘制区域是否在padding内。

    比较巧妙的一个用法是,在RecyclerView中:

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingTop="8dp"
            android:clipToPadding="false" />
    

    这样设置,可以让RecyclerView在第一次展示时顶部留出8dp的空白,如果往上滑动,Item可以填充到顶部的padding内。

    clipChildren:
    Defines whether a child is limited to draw inside of its bounds or not. This is useful with animations that scale the size of the children to more than 100% for instance. In such a case, this property should be set to false to allow the children to draw outside of their bounds. The default value of this property is true.

    大概翻译一下:是否将子View的绘制限制在父View的边界内。这个属性可以用于缩放动画的例子,将这个属性设置为false,以允许子View的绘制超出父View的边界。这个属性默认值为true。

    官方文档已经说的十分清楚了,唯一需要注意的是:使用该属性时,需要将android:clipChildren="false"放在布局的根节点才会生效。

    9.maxLength和maxEms的区别

    maxLength:
    Set an input filter to constrain the text length to the specified number.
    翻译:通过设置一个InputFilter来限制文本长度为一个指定的值。

    补充说明:

    • 汉字、英文字母、标点以及空格都只占用一个长度
    • 由于对文本长度进行了截取,所以不会显示省略号

    maxEms:
    Makes the TextView be at most this many ems wide.
    翻译:设置TextView的最大宽度为N个em的宽度。

    ems其实是em单位的复数形式,em单位可以参见维基百科(科学上网)
    维基百科中的解释并不明确,Android官方文档中也没有相关说明
    但是笔者通过试验,排除掉了一种解释:

    em是一个印刷排版的单位,em字面意思为:equal M(和M字符一致的宽度为一个单位)简称em
    // 后面已经证明这个解释在Android中并不适用

    我们来做一下测试:
    设置android:maxEms="5" 然后填入一些内容:

    内容 最大个数
    数字 10
    字母a 10
    字母i 23
    字母M 6
    英文逗号 31
    汉字 5

    设置android:maxEms="10" 然后填入一些内容:

    内容 最大 个数
    数字 21
    字母a 21
    字母i 47
    字母M 13
    英文逗号 63
    汉字 11

    我只能说,这是一个并不十分精确的单位...

    个人觉得最靠谱的一种解释是(仅供参考):

    em 等于当前的字体尺寸,如果字体大小是16px,那么1em=16px。

    这其实是css中关于em单位的解释,猜测安卓应该也差不多。

    补充说明:

    • maxEms只对TextView生效,如果要限制EditText的文本长度,应该使用maxLength
    • maxEms是对TextView的最大宽度做了限制,而不是对文本内容进行截取,所以maxEms是支持省略号的
    • maxEms只在 android:layout_width="wrap_content" 时生效

    10.关于18 : 9屏幕开屏图的适配

    如今全面屏盛行,屏幕比例大都是18 : 9(好像只有三星是18.5 : 9),适配起来也不是太困难,只需要在AndroidManifest文件中加入如下代码即可解决大部分问题:

        <meta-data
            android:name="android.max_aspect"
            android:value="2.1"/>
    

    平常使用的第三方图片加载库也都支持各种缩放模式,但是唯独遗漏了一个地方,那就是开屏图,可以看到在18 : 9的手机上,开屏图会拉伸变形:


    淘宝开屏图拉伸变形,此图来自小米开发者中心

    究其原因,是在开屏图的实现方式上。我们实现应用的开屏图,一个比较简单常见的方法就是,给LaunchActivity设置一个主题,然后在主题中设置背景图:

        <activity
            android:name=".view.activity.LaunchActivity"
            android:screenOrientation="portrait"
            android:theme="@style/LaunchTheme">
    
        <style name="LaunchTheme" parent="AppTheme">
            <item name="android:windowBackground">@mipmap/c_boot_bg</item>
            <item name="android:windowFullscreen">true</item>
        </style>
    

    这种方式实现起来很方便,但是弊端是这种方式设置的图片,不支持各种缩放模式,我们只能在不同的资源目录下放上相应大小的图片,让系统去自动选取最合适的图片。

    于是,当开屏图被拉伸后,我就在资源文件目录下新建了一个mipmap-xxhdpi-2160x1080目录,然后放入相应的图片,打开模拟器新建了一个2160x1080 5.99寸屏幕的设备(公司不给配测试机,我基本都在模拟器上搞)试了一下没问题,但是在CTO的手机上(也是18 : 9的屏幕)却还是被拉伸了,说明我新加的图片资源没有匹配上,这就奇怪了,CTO手机的分辨率和尺寸跟我的模拟器一模一样,为什么一个能适配,一个不能呢?
    后来我找到了问题的原因:屏幕下方的虚拟按键
    由于全面屏手机追求窄边框,正面大都是没有按钮的,所以必须启用虚拟按键,而这个虚拟按键是要占用屏幕空间的。我们先来看一看系统是如何匹配资源目录的,这里我就不做实验了,直接上结论:

    dpi范围 密度 手机分辨率
    0dpi ~ 120dpi ldpi 320 x 240
    120dpi ~ 160dpi mdpi 480 x 320
    160dpi ~ 240dpi hdpi 800 x 480
    240dpi ~ 320dpi xhdpi 1280 x 720
    320dpi ~ 480dpi xxhdpi 1920 x 1080
    480dpi ~ 640dpi xxxhdpi 2560 x 1440

    以下内容引用自:http://blog.csdn.net/cloud_castle/article/details/52313858

    如果当前为xhdpi设备,并且只有以下几个目录,则drawable的寻找顺序为:
    xhdpi -> xxhdpi -> xxxhdpi(如果没有更高的了) -> nodpi(如果有的话) -> hdpi -> mdpi,如果在xxhdpi中找到目标图片,则压缩2/3来使用,如果在mdpi中找到图片,则放大2倍来使用。

    如果当前设备为xhdpi-1280x800,当前目录有values-xhdpi-1280x800,values-xhdpi-1280x960,values-xhdpi-1280x720,则寻找顺序为:
    values-xhdpi-1280x800 -> values-xhdpi-1280x720 -> values-xhdpi。
    只向等于或低于自己分辨率的目录下寻找,直到values-xhdpi,如果依然没有找到,按照之前的顺序继续进行。(hdpi-1280x800 -> hdpi-1280x720 -> hdpi -> …)

    结合上面的结论,当全面屏手机开启虚拟按键后,扣除掉虚拟按键占用的尺寸(在原生系统、5.99寸屏幕、2160x1080的手机上,虚拟按键高度为144px)屏幕尺寸约为2016x1080 ,注意现在是2016x1080,之前我建的资源目录是2160x1080,根据上面的匹配规则,是不会去匹配比自己分辨率高的资源目录的,所以才出现了之前的适配问题。
    所以,我们只需要再建一个mipmap-xxhdpi-2016x1080的目录,问题就得到解决了。通过这个问题让我对资源目录的匹配规则也有了更深入的了解。

    11.使用getIdentifier()方法获取资源

        /**
         * Return a resource identifier for the given resource name.  A fully
         * qualified resource name is of the form "package:type/entry".  The first
         * two components (package and type) are optional if defType and
         * defPackage, respectively, are specified here.
         * 
         * <p>Note: use of this function is discouraged.  It is much more
         * efficient to retrieve resources by identifier than by name.
         * 
         * @param name The name of the desired resource.
         * @param defType Optional default resource type to find, if "type/" is
         *                not included in the name.  Can be null to require an
         *                explicit type.
         * @param defPackage Optional default package to find, if "package:" is
         *                   not included in the name.  Can be null to require an
         *                   explicit package.
         * 
         * @return int The associated resource identifier.  Returns 0 if no such
         *         resource was found.  (0 is not a valid resource ID.)
         */
        public int getIdentifier(String name, String defType, String defPackage) {
            return mResourcesImpl.getIdentifier(name, defType, defPackage);
        }
    

    这个方法虽然在性能上差一些,但有时候用起来真的很省事,举个例子:
    当服务端返回一个枚举值,并且这个枚举值不能直接用来展示,你得在strings.xml文件里找到相应的值用来做展示。使用如下的方式,可以让代码逻辑变得简单,而不用去写一堆 switch 代码了:

    //data为服务端返回的数据
    int resourceID = context.getResources().getIdentifier(data.getLanguage(), "string", context.getPackageName());
    tvLangeage.setText(context.getString(resourceID));
    

    12.SharedPreferences 中 commit() 和 apply() 方法的区别

    commit() 和apply() 虽然都是原子操作,但是原子的操作不同。

    • commit() 方法是原子操作提交到数据库,所以从提交数据到存入disk都是同步过程,中间不可打断,有返回值。
    • apply() 方法的原子操作是原子提交到内存中,而非数据库,所以在提交到内存中时不可打断,之后再异步提交到数据库中,因此没有相应的返回值。

    所有commit() 提交都是同步过程,效率会比apply() 异步提交的速度慢,但是apply() 没有返回值,永远无法知道存储是否成功。

    结论:在不关心提交结果是否成功并且没有后续操作的情况下,优先考虑apply() 方法。

    13.onAttachedToWindow() 和 onDetachedFromWindow()

    View里的:
    onAttachedToWindow() 是在第一次 onDraw() 前调用,可以进行一些初始化操作
    onDetachedFromWindow() 可以在这个方法做一些收尾工作

    14.onDraw() 和 dispatchDraw()

    1. 自定义一个view时,重写onDraw。
      调用view.invalidate(),会触发onDraw和computeScroll()。前提是该view被附加在当前窗口上
      view.postInvalidate(); //是在非UI线程上调用的
    1. 自定义一个ViewGroup,重写onDraw。
      onDraw可能不会被调用,原因是需要先设置一个背景(颜色或图)。
      表示这个group有东西需要绘制了,才会触发draw,之后是onDraw。
      因此,一般直接重写dispatchDraw来绘制viewGroup

    15.未完待续...

    相关文章

      网友评论

        本文标题:实例波的Android备忘录

        本文链接:https://www.haomeiwen.com/subject/sxkiuxtx.html