Android开发细节

Android开发中有一些小的知识点,了解了不仅能能够避免很多坑,同时也能够简化开发过程,提高代码的健壮性。

分割线

分割线是Android开发中一个常用的UI元素,我们可通过定义一个View,并设置背景来实现,但其实AbsListView和LinearLayout都提供了分割线的功能:

AbsListView

AbsListView通过divider和dividerHeight来控制分割线的样式,通常情况下这两个属性就够了,但是当需要设置分割线的边距时,就无法满足需求了,这时可通过2种方式来实现:

  • 隐藏ListView默认的分割线,在item_layout中添加一个View作为分割线;
  • 自定义Drawable作为分割线;

第一种方式需要修改item_layout布局, 不推荐。这里我们使用第二种方案:

<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
    android:insetLeft="24dp"
    android:insetRight="24dp">
    <shape android:shape="rectangle">
        <solid android:color="#e0e0e0"/>
        <size android:height="1px"/>
    </shape>
</inset>

通过insetLeft和insetRight我们能够很容易地控制分割线的边距。接下来只需要将这个Drawable资源引用到divider属性中即可。

这里顺便说一下分割线资源的命名,在开发中可能会有多个分割线样式,为了便于复用,推荐使用以下命名方式:

divider_color_left_right  
// 如:divider_e0e0e0_24dp_12dp, 表示分割线颜色为#e0e0e0, 左边距为24dp, 右边距为12dp

LinearLayout

LinearLayout中对于分割线提供了3个属性:

divider:分割线的样式,取值为颜色值或drawable
showDividers: 分割线的位置,取值为:beginning, middle, end,依次表示第一个childView前面的分割线,childView之间的分割线,最后一个chidView后面的分割线,所以可通过这三个值的组合来实现需要效果;
dividerPadding: 分割线的Padding,如果LinearLayout为垂直的,则表示分割线的左右padding, 否则表示上下padding;

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:divider="#eee"
    android:showDividers="middle"
    android:dividerPadding="24dp"
    android:orientation="vertical">
    ...
</LinearLayout>

虽然LinearLayout的分割线可以通过dividerPadding设置边距,但设置的是两边的,如果只需要左边距为24dp的分割线,那就只能将divider设置为上面自定义的分割线,在Drawable中设置边距。

黑白屏问题

默认情况下,APP在启动时会有一个黑屏/白屏(依据主题而定),解决方案通常有2中:

  • 使用透明背景,如微信;
  • 使用启动页的图片作为背景;

使用透明背景, 也就是将主题的背景设置为透明。这种方式有点甩锅的意思(既然是你Android自己的问题,那你就背锅吧),体验度可以说很差,点击了APP图标,1s之后才有反应,会让用户以为手机卡了呢?另一种实现方式,是目前大多数APP采用的方案,使用起来也很简单,只需要自定义一个主题,将背景设置为启动页的图片,并应用于SplashActivity。

<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowFullscreen">false</item>
    <item name="android:windowBackground">@drawable/splash_bg</item>
</style>

但这种直接使用启动页图片的方式也存在1个明显的缺陷:需要针对不同的屏幕适配不同的尺寸的图片;而这种图片一般都很大,所以无形中增加了APK的大小。

那有没有更好的实现方案呢?分析一下当前APP启动页的主流设计,启动页的设计无非3个元素:背景,Logo, 宣传语。既然这样,那可以通过定义Drawable资源文件来实现,通过layer-list叠加这3个元素,达到设计图的效果。

<?xml version=”1.0” encoding=”utf-8”?>










使用这种方式的好处是:Logo, 宣传语不会变形,即便只有一组切图。当然,对于比较复杂的启动页,还是直接使用多套切图来实现。

Toast的使用

Toast是Android开发中常用的一个小组件, 使用的时候最好将其封装一下,主要解决以下问题:

  • 简化调用,直接传入提示信息即可;
  • 降低耦合,实现样式的自定义;
  • 共享Toast对象,解决快速点击导致提示长时间显示的问题;

具体的代码可参考:ToastUtil

页面跳转

在开发中,有时候会遇到这样的需求,在A页面点击下一步跳转到C页面,按返回键后返回到B页面,这个时候startActivities就该排上用场了,startActivities会将指定的Activity全部加入到Activity Task中,并启动最后一个Activity:

Intent[] intent = new Intent[2];
intent[0] = new Intent(this, BActiity.class);
intent[1] = new Intent(this, CActivity.class);
startActivities(intent);

在跳转到第三方APP的时候,应该判断指定的Activity是否存在,否则可能会出现ActivityNotFoundException。

if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
    startActivity(intent);
} else {
    ToastUtil.show(this, "没有找到指定的Activity~");
}

或者使用Intent的resolveActivity方法:

if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(intent);
} else {
    ToastUtil.show(this, "没有找到指定的Activity~");
}

clipPadding属性

默认情况下,padding属性所占的空间不在可滑动的范围内,我们可通过将clipPadding属性设置为false,将padding所占用的空间加入到可滑动的部分,这在可滑动的View中非常有用。比如,ListView的头部需要留60dp的空白,我们可通过padding属性来实现:

android:paddingTop="60dp"
android:clipPadding="false"

而不是添加一个高度为60dp的headerView。

Context的使用

Context是我们常用的类,但在使用时需要注意以下几点:

  • 在单例或静态方法中,优先使用ApplcationContext类型的Context;
  • 在Activity或Service中定义内部类时,需定义成静态的,因为非静态内部类持有外部类的引用,会引起内存泄漏;
  • 多进程的Application中的onCreate()会被执行多次,所以需要对其进行进程隔离,即:在各自的进程中执行各自的任务;
if (getPackageName().equals(AppUtil.getProcessName(this))) {
    ...
} else if ("processId".equals(AppUtil.getProcessName(this))) {
    ...
}

文字单位

Google推荐文字的单位使用sp, 但是sp不仅跟屏幕密度有关,还与系统设置的字体大小有关,所以如果要使用sp, 对于TextView及其子类,以及所在的ViewGroup,应避免使用固定的宽度和高度,同时需要对修改系统文字大小进行兼容性测试,避免布局出现重叠问题。所以,为了更好的兼容性,以及UI的一致性,推荐使用dp作为文字的单位。

Resource的使用

使用系统资源时,优先使用Resource.getSystem(), 可避免对Context的引用。如获取屏幕参数的工具类:

public class DisplayUtil {

    public static int getScreenWidth() {
        return Resources.getSystem().getDisplayMetrics().widthPixels;
    }

    public static int getScreenHeight() {
        return Resources.getSystem().getDisplayMetrics().heightPixels;
    }

    public static float getScreenDensity() {
        return Resources.getSystem().getDisplayMetrics().density;
    }

    public static int dpToPx(int dp) {
        return Math.round(Resources.getSystem().getDisplayMetrics().density * dp + 0.5f);
    }

    public static int spToPx(int sp) {
        return Math.round(Resources.getSystem().getDisplayMetrics().scaledDensity * sp + 0.5f);
    }

    public static int pxToDp(int px) {
        return Math.round(px / Resources.getSystem().getDisplayMetrics().density + 0.5f);
    }

    public static int pxToSp(int px) {
        return Math.round(px / Resources.getSystem().getDisplayMetrics().scaledDensity + 0.5f);
    }

    public static int getStatusHeight(Context context) {
        Rect rect = new Rect();
        ((Activity)context).getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
        return rect.top;
    }

    public static String getScreenParams(Context context) {
        return getScreenWidth() + "*" + getScreenHeight();
    }
}

在Fragment中避免直接使用getResource()获取Resource对象,容易出现IllegalStateException异常【提示:Fragment XXX not attached to Activity】。应直接获取Activity的Resource对象,至于getActivity()为nulld的问题可使用下面的方式解决:

@Override
public void onAttach(Context context) {
    super.onAttach(context);
    // 解决getActivity()为nulld的情况
    this.mContext = (FragmentActivity) context;
}

// 获取Resource对象
mContext.getResource();

Adapter的设置

在设置Adapter的时候,通常都会复用View,但是在复用的时候需要注意:在条件判断时,需要完整的分支结构,即有if就应该有else; 否则就会出现复用问题。

Dialog的使用

Dialog也是开发中使用频率很高的一个组件,通常我们使用的都是自定义的AlertDialog, 但是在使用过程中会出现很多问题,比如:Activity销毁的时候没有关闭Dialog;延迟显示Dialog时Activity被销毁了等等。所以,可以使用DialogFragment(本质为Fragment)封装一下AlertDialog,使其生命周期跟随Activity的生命周期而变化,从而简化了对Dialog的管理。

自定义ellipse样式

TextView的ellipse属性时开发中常用的一个属性,以解决TextView中的文本较长时以…的样式显示。但偶尔可能遇到需修改这种显示样式的需求,比如将”…”更换为” 更多”,可通过下面的方式来解决:

// 计算需要追加的文字宽度
private void getMoreTextWidth() {
    TextView textView = new TextView(this);
    textView.setTextSize(14);
    TextPaint paint = textView.getPaint();
    float[] items = new float[moreText.length()];
    paint.getTextWidths(moreText, 0, moreText.length(), items);
    for (float item : items) {
        moreTextWidth += item;
    }
}

// 将文字追加到指定行的指定位置
private void setEllipsizeStyle(final TextView commentText, final int maxLine, final String comment) {
    commentText.setText(comment);
    commentText.post(new Runnable() {
        @Override
        public void run() {
            int lineCount = commentText.getLineCount();
            if (lineCount <= maxLine) {
                return;
            }

            Layout textLayout = commentText.getLayout();
            // 获取指定行最后一个字符的位置索引
            int lastIndex = textLayout.getLineEnd(maxLine);
            // 获取指定行的开始位置和结束位置
            float lineLeft = textLayout.getLineLeft(maxLine);
            float lineRight = textLayout.getLineRight(maxLine);
            StringBuilder builder = new StringBuilder();
            if (lineRight - lineLeft + moreTextWidth > maxWidth) { // 直接添加[更多]
                lastIndex -= moreText.length();
            } else {
                String lastChar = comment.substring(lastIndex - 1, lastIndex);
                if ("\n".equals(lastChar)) {
                    lastIndex--;
                }
            }
            builder.append(comment.substring(0, lastIndex)).append(moreText);
            commentText.setText(builder);
        }
    });
}

设置View背景

设置圆角矩形的背景,通常我们会使用自定义Drawable资源来实现,但是这种方式很麻烦,不同的圆角,背景色就需要定义不同的资源文件,随着开发的迭代,Drawable资源越来越多,更糟糕的是,对于selector类的样式,可能需要定义三个资源来实现,相对来说很麻烦。其实我们可以通过GradientDrawable来设置背景,圆角,渐变等。这里写了一个工具类——ViewBgUtil,随后需要设置背景时只需要调用一句代码即可实现。

设置文字样式

开发过程中可能经常会遇到一行文本需要多种样式的情况,如:价格:¥20, ¥符号的字体为14dp, 其他的问题为16dp, 这时总不至于使用3个TextView来实现吧。对于这种情况,可使用Android的富文本——SpanningString来实现。这里写了个工具类——SpanStyleUtil,可简化设置过程。

布局嵌套问题

尽可能杜绝ScrollView嵌套ListView的情况,默认情况下这种嵌套会导致ListView只显示一个Item,所以为了计算高度,需要重写ListViewd的onMeasure()方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
    MeasureSpec.AT_MOST);
    super.onMeasure(widthMeasureSpec, expandSpec);
}

但这种计算方式会导致ListView的高度为Item的总高度,而不是屏幕的高度,这也就是说,复用没什么卵用了。虽然页面滑动的过程中不会调用getView, 感觉好像丝丝顺滑,但是加载的过程太慢,因为它会一次性将所有的数据加入进来。一旦列表中包含图片,就很容易引发OOM, 所以,应直接杜绝这种实现方式,使用ListView+HeaderView+FooterView或使用RecycleView来实现相应的需求。

总结

以上是Android开发中的一些细节知识,由于每个细节无法自成文章,所以就整理起来了。随后可能会继续完善,当然,也欢迎各位补充。

,