安卓开发过程中,经常会出现一些比较麻烦的情况,并不是说难以解决,只是有时候的解决方法会让代码看起来像是玩具一般,生怕一不小心就crash掉,这里列出一些常见的麻烦;
针对不同的情况可能这些方法不是万能钥匙,只是提供一些解决的想法
很多时候,我们需要向用户展示一些信息,比如这种情况:
或者说是这种情况:
可以看到,对于这种行式文本信息,一般来说前面是一个label,不会改变,后面紧跟结果值;
通常时候我们需要这样来处理:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
style="@style/TextViewStandard"
android:text="label:"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
style="@style/TextViewStandard"
android:text="I'm value"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
LinearLayout>
若是简单的布局还好,若是本来布局就比较复杂,或者在使用了ConstraintLayout布局的情况下,如此处理就会使得布局臃肿,在进行measure、layout和draw过程中,会很影响性能。
对于安卓原生的View来说,TextView的功能强大的令人发指;
其中就包括了 drawable***(drawableRight等属性)以及drawablePadding,如果可以将 label部分变成一个drawable,然后设置到textview的上下左右位置,那么对于以上情景出现的文本信息,一个控件就足以解决。
在布局里面这样(和普通方式没区别):
id="@+id/tv_text"
style="@style/TextViewStandard"
android:text="I'm value"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
然后在activity中这样:
((TextView) findViewById(R.id.tv_text)).setCompoundDrawables(ColorTextDrawable.getTextDrawable(context, Color.RED,18),null,null,null);
对于那种左右上下都有label的界面,这种方法会方便很多,并且可以动态的去调整drawable中文字的大小颜色。
并且ColorTextDrawable类也很简单,基本没有什么复杂的操作,这里直接贴上源码:
/**
* Created on 2018/1/17
* function : 文本形式的drawable
*/
public class ColorTextDrawable extends Drawable {
private static final String TAG = "TextDrawable";
private Context context;
private TextPaint textPaint;
private CharSequence charSequence;
private int textSize;
private Rect rect;
private int alpha;
private int color;
private int xOffset;
private int yOffset;
private ColorTextDrawable(Context context) {
this.context = context;
textPaint = new TextPaint();
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.LEFT);
charSequence = "";
}
private ColorTextDrawable(Context context, int xOffset, int yOffset) {
this(context);
this.xOffset = xOffset;
this.yOffset = yOffset;
}
/**
* @param context 上下文
* @param text 文本
* @param color 颜色
* @param textSize 字体大小
* @return drawable资源
*/
public static ColorTextDrawable getTextDrawable(Context context, String text, int color, int textSize) {
ColorTextDrawable textDrawable = new ColorTextDrawable(context)
.setText(text)
.setColor(color)
.setTextSize(textSize);
textDrawable.setBounds(0, 0, textDrawable.getIntrinsicWidth(), textDrawable.getIntrinsicHeight());
return textDrawable;
}
public static ColorTextDrawable getTextDrawable(Context context, String text, int color, int textSize, int xOffset, int yOffset) {
ColorTextDrawable textDrawable = new ColorTextDrawable(context, xOffset, yOffset)
.setText(text)
.setColor(color)
.setTextSize(textSize);
textDrawable.setBounds(0, 0, textDrawable.getIntrinsicWidth() + Math.abs(xOffset), textDrawable.getIntrinsicHeight() + Math.abs(yOffset));
return textDrawable;
}
@Override
public void draw(Canvas canvas) {
textPaint.setTextSize(textSize);
canvas.drawText(charSequence, 0, charSequence.length(), xOffset, yOffset - textPaint.getFontMetrics().top, textPaint);
}
@Override
public void setBounds(Rect bounds) {
super.setBounds(bounds);
this.rect = bounds;
}
@Override
public void setBounds(int left, int top, int right, int bottom) {
super.setBounds(left, top, right, bottom);
this.rect = new Rect(left, top, right, bottom);
}
@Override
public void setAlpha(int alpha) {
this.alpha = alpha;
textPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
textPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public int getIntrinsicWidth() {
return (int) textPaint.measureText(charSequence.toString());
}
@Override
public int getIntrinsicHeight() {
return (int) (textPaint.getFontMetrics().bottom - textPaint.getFontMetrics().top);
}
@Override
public int getMinimumWidth() {
return getIntrinsicWidth();
}
@Override
public int getMinimumHeight() {
return getIntrinsicHeight();
}
public ColorTextDrawable setTextSize(int textSize) {
this.textSize = textSize;
textPaint.setTextSize(textSize);
return this;
}
public CharSequence getText() {
return this.charSequence;
}
public ColorTextDrawable setText(CharSequence charSequence) {
this.charSequence = charSequence;
return this;
}
public ColorTextDrawable setColor(int color) {
this.color = color;
textPaint.setColor(color);
return this;
}
public int getxOffset() {
return xOffset;
}
public void setxOffset(int xOffset) {
this.xOffset = xOffset;
}
public int getyOffset() {
return yOffset;
}
public void setyOffset(int yOffset) {
this.yOffset = yOffset;
}
}
其中对getTextDrawable进行一次重载,可以用来设定x和y方向的偏移量,偏移量为正表示向右向下(安卓坐标轴正方向);
如果是一些携带了图片的文本信息,那么单纯的使用 ColorTextDrawable 是无法完成需求的,这时候就需要使用复杂式菜单布局——LineMenuView;
这个控件可以完成以下功能:
实际view树并没有减少层次,不过因为多了一层封装,至少布局文件可以看起来简单一些。
github地址:LineMenuView
安卓开发中,列表视图基本算是最复杂的一种,ListView,GridView,RecyclerView;
一般来说,对于一些记录性的内容,都需要使用ListView或者RecyclerView进行滚动显示,像以下这种情况:
需要同时显示三列信息,但是中间“领取时间”一列在我们搬运代码时无法确定宽度,这样标题的位置可能会错位。
LinearLayout提供了weight属性,可以将总宽度n等分,这样可以保证下面每个Item的宽度和表头一样。
想下面这样,表头布局和Item布局使用相同代码
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
style="@style/TextViewStandard"
android:layout_weight="1"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="奖励类别"/>
<TextView
android:layout_weight="1"
android:gravity="center"
style="@style/TextViewStandard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="领取时间"/>
<TextView
android:gravity="center"
style="@style/TextViewStandard"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="奖励金额"/>
LinearLayout>
这样肯定可以保证宽度的统一,但如此依赖,不说weight值很难确定,同样的Item无法根据自己真实数据的宽度来动态改变列的宽度。
使用表格的形式,强行固定列宽;比如使用TableLayout来显示数据,不过因为该布局没有一般AdapterView的特性,很容易子布局数量膨胀爆炸。
使用GridView的话。。。,需要自己添加数条数据,说起来还不如使用LiearLayout来的方便
先把回调接口贴出来,然后说明有什么用处:
/**
* Created on 2018/3/6
* function : 当容器类组件中item有布局变化时,通知刷新布局效果
*/
public interface CallBackAfterItemMeasure {
/**
* 标志位
*/
int TAG_ONE = 1;
int TAG_TWO = 2;
int TAG_THREE = 3;
/**
* @param tag 若是同时检测多个宽高,则用于区分
* @param measureWidth item中想要获取的view的宽度
* @param measureHeight item中想要获取的view的高度
* @return true则表示此次已经完成了测量, 以后都不需要再调用该方法了;
* false则表示每次刷新视图都需要实现类执行该方法完成宽度和高度的设置
*/
boolean doAfterItemMeasure(int tag, int measureWidth, int measureHeight);
}
看到该类一般可以明白个大概,就是在 item 加入AdapterView中时(填充数据后),主动请求表头布局来刷新自身的宽度,假设情景模式中的布局是RecyclerView:
Item对应的xml文件和表头布局文件使用相同的代码:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_"
style="@style/TextViewStandard"
android:layout_weight="1"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="奖励类别"/>
<TextView
android:id="@+id/tv_time"
android:gravity="center"
style="@style/TextViewStandard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="领取时间"/>
<TextView
android:gravity="center"
style="@style/TextViewStandard"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="奖励金额"/>
LinearLayout>
可以看到,中间列宽设定为:wrap_content
两侧两列设置weight平分。
那么在Adapter的onBindViewHolder方法中就可以这么搞:
/**
* 是否禁止处理callback
*/
private volatile boolean forbid;
@Override
public void onBindViewHolder(TestAdapter.ViewHolder holder, int position) {
//... 刷新布局数据
if(callback!=null&&!forbid) {
postMeasure(holder.mTvTime,CallBackAfterItemMeasure.TAG_ONE);
}
}
/**
* 通知callback执行
*/
private void postMeasure(View view,int tag){
view.post(() -> {
if(forbid){
return;
}
int measuredWidth = view.getMeasuredWidth();
int measuredHeight =view.getMeasuredHeight();
//只有宽度和高度为有效值时,才会进行调用
if(measuredHeight>0&&measuredWidth>0&&callback.doAfterItemMeasure(tag,measuredWidth,measuredHeight)) {
forbid=true;
}
});
}
其中callback是指实现了CallBackAfterItemMeasure接口的Activity类,这样只要 id 为tv_time的TextView宽度一可能有变化,就通知Activity去刷新表头列的宽度:
public class TestActivity implements CallBackAfterItemMeasure{
// ... 其他代码
/**
* @param tag 若是同时检测多个宽高,则用于区分
* @param measureWidth item中想要获取的view的宽度
* @param measureHeight item中想要获取的view的高度
* @return true 则表示此次已经完成了测量, 以后都不需要再调用该方法了
* false 则表示每次刷新视图都需要实现类执行该方法完成宽度和高度的设置
*/
@Override
public boolean doAfterItemMeasure(int tag, int measureWidth, int measureHeight) {
if (tag == CallBackAfterItemMeasure.TAG_ONE && measureWidth > mTvLabelTime.getMeasuredWidth()) {
mTvLabelTime.setWidth(measureWidth);
}
return false;
}
}
逻辑也很简单,保证 表头中间列 的宽度和 Item中间列的宽度相同。
表头两边的两列因为weight的关系,和Item中两侧的两列宽度肯定是相同的。
这是一种思路,只要设置了宽度为wrap_content,那么可以同时来控制很多表头列的宽度和Item布局列宽度相同,从而达到显示效果统一的目的。
注:当然应该看到的是,这里选取id为tv_time有取巧成分,因为对于时间格式,它们的长度基本是相同的,每个item对应的时间列不会有什么变化。当然之前也说了,有了这种“反馈机制”,只要认真考虑,总可以找到很好的监听方案。
安卓里面有三种最常用的提示方法:Toast,SnackBar,Dialog(为了安全起见,使用DialogFragment也是可以的)
当然还有一种重量级提示:Notification
现在暂时不考虑SnackBar和Notifacation,一个因为是必须依赖于现有界面;一个则是太“量”,不方便使用。
在应用中,经常会出现那种上一个Toast还未消失,下一个Toast已经显示的情况;
或者说如果多个界面中需要弹出Dialog,不能每个界面都写代码吧。
这时就可以这样:
RxBus.getInstance().toObservable(BaseEvent.class).observeOn(AndroidSchedulers.mainThread()).subscribe(baseEvent -> {
switch (baseEvent.operateCode) {
//弹出提示
case Const.SHOW_TOAST: {
singleSmallToast.setText(baseEvent.data.toString());
singleSmallToast.show();
break;
}
//显示登录超时
case Const.SHOW_LOGIN_DIALOG: {
//在当前栈顶不是登录界面情况下,再去弹出登录窗口
if (!(topActivity instanceof LoginActivity)) {
DoLoginDialog.getInstance(topActivity).show();
}else{
RxBus.getInstance().post(new BaseEvent(Const.SHOW_TOAST,"登录失败,请重新登录"));
}
break;
}
}
});
RxBus框架,代码简单,基于RxAndroid,简单好用;
singleSmallToast就是一个普通的toast。
代码:
DoLoginDialog.getInstance(topActivity).show();
就是显示一个登录框而已。
至于topActivity则可以通过监听activity声明周期得到(Application类的方法):
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
// ... 其他代码
@Override
public void onActivityResumed(Activity activity) {
topActivity = activity;
}
// ... 其他代码
});
这里贴上RxBus的源码:源码地址
/**
* 功能----RxBus处理数据总线
*/
public class RxBus {
private static volatile RxBus defaultInstance;
private final Subject
现在有这种要求:
给定一个图片,需要显示在ImageView中,且不论图片是什么大小形状,都需要保证ImageView显示的宽高比。
针对这个情况:
我们都是按照UI给的设计图来的,拿米尺量一量,像这样:
<ImageView
android:src="@drawable/placeholder"
android:layout_width="200dp"
android:layout_height="100dp"/>
完美!肯定不会变形,这种情况下,ImageView不管如何测量,不仅长宽比,连长宽都不会变。
基本满足了需求,就是有点生硬,在不同设备上,显示的会有些区别。
不过总的来说,至少是总不错的解决方案。
.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
"0dp"
android:layout_height="0dp"
android:src="@drawable/placeholder"
app:layout_constraintDimensionRatio="2:1"/>
.support.constraint.ConstraintLayout>
没错就是这个:
app:layout_constraintDimensionRatio="2:1"
可以用来设定宽高比例,这样一来,图片拥有了宽高的可变性,又保持了宽高比例,相当的完美。
只不过如果本来不打算使用ConstraintLayout的话,这样做就会导致view树层级加深,会稍微影响一下性能。
最后一种就比较正规了,我们可以定义一种只能保持一种长宽比的ImageView,比如说:
RectImageView布局(正方形,宽高1:1):
/**
* Created on 2018/3/27
* function : 宽高相同的ImageView
*/
public class RectImageView extends AppCompatImageView {
public RectImageView(Context context) {
super(context);
}
public RectImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RectImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//让高度等于宽度
if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
}
}
}
什么都不理,直接在测量结束之后修改测量结果,“欺骗”view的onMeasure流程。
这样的话,xml中怎么写区别都不大,只要不为0就行:
<com.test.view.RectImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/placeholder"/>
这个问题由来已久,不过大多数都是因为UI切图只给了IOS的,最后安卓照着做,还得学着ios将标题居中。
这个就比较扯了,因为Toolbar中固有逻辑只是将Title文本设置到左侧,并且控件基于动态生成,具体可以看源码:
Toolbar类setTitle方法:
/**
* Set the title of this toolbar.
*
* A title should be used as the anchor for a section of content. It should
* describe or name the content being viewed.
*
* @param title Title to set
*/
public void setTitle(CharSequence title) {
if (!TextUtils.isEmpty(title)) {
if (mTitleTextView == null) {
final Context context = getContext();
mTitleTextView = new AppCompatTextView(context);
mTitleTextView.setSingleLine();
mTitleTextView.setEllipsize(TextUtils.TruncateAt.END);
if (mTitleTextAppearance != 0) {
mTitleTextView.setTextAppearance(context, mTitleTextAppearance);
}
if (mTitleTextColor != 0) {
mTitleTextView.setTextColor(mTitleTextColor);
}
}
if (!isChildOrHidden(mTitleTextView)) {
addSystemView(mTitleTextView, true);
}
} else if (mTitleTextView != null && isChildOrHidden(mTitleTextView)) {
removeView(mTitleTextView);
mHiddenViews.remove(mTitleTextView);
}
if (mTitleTextView != null) {
mTitleTextView.setText(title);
}
mTitleText = title;
}
要修改这个,只能说麻烦,根本没有提供外部修改标题Gravity方法。
这个也是网上说的最多的一种方法,也就是自己搞一个TextView出来,同时设置AndroidManifest中Activity的label为空字符串,让系统标题无法显示;
并且代码中不能调用:
Activity.setTitle()
方法。
每次修改标题时,自己设置自定义的TextView内容。。。
详情可以参考:其他博客
好吧说到底其实上面方法可以很完美的解决居中问题,自定义Toolbar也和之前内容没有多大区别,基本就是这样子:
在xml中这样使用:
<com.test.view.TitleCenterToolbar
android:transitionName="toolbar"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
app:navigationIcon="@drawable/icon_back"
android:theme="@style/ToolbarTheme"/>
CenterToolbar基本和原来一样:
public class TitleCenterToolbar extends Toolbar {
@BindView(R.id.tv_title)
TextView mTvTitle;
public TitleCenterToolbar(Context context) {
this(context, null);
}
public TitleCenterToolbar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, android.support.v7.appcompat.R.attr.toolbarStyle);
}
public TitleCenterToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
/**
* 初始化数据
*/
private void initView() {
inflate(getContext(), R.layout.layout_title_center_toolbar, this);
ButterKnife.bind(this);
}
@Override
public CharSequence getTitle() {
return TextUtils.isEmpty(mTvTitle.getText())?null:mTvTitle.getText();
}
@Override
public void setTitle(int resId) {
mTvTitle.setText(resId);
}
@Override
public void setTitle(CharSequence title) {
mTvTitle.setText(title);
}
@Override
public void setTitleTextAppearance(Context context, int resId) {
mTvTitle.setTextAppearance(context, resId);
}
@Override
public void setTitleTextColor(int color) {
mTvTitle.setTextColor(color);
}
}
layout_title_center_toolbar其实就是一个TextView:
<android.support.v7.widget.AppCompatTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tv_title"
style="@style/Base.TextAppearance.Widget.AppCompat.Toolbar.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:paddingStart="@dimen/view_padding_margin_8dp"
android:paddingEnd="@dimen/view_padding_margin_8dp"
android:layout_gravity="center"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
tools:text="标题"/>
很简单,只是修改了setTitle方法固有的逻辑,不会调用super,系统自然不会自动成功Title对应的TextView
然后如果每次需要修改标题的话,直接使用
Activity.setTitle()
就可以了。
注:
如果在代码中调用了:
Activity. setSupportActionBar(toolbar);
那么就尽量不要自己调用toolbar.setTitle方法,因为无法保证自己对Toolbar的setTitle方法调用和系统自动对Toolbar的setTitle方法调用谁先执行。
这个不需要过多说明,安卓中太多的界面跳转,如果只是手搓代码来执行,那么即便不觉得繁琐;
也会因为跳转逻辑的复杂导致代码混乱不堪。
还有,考虑一个问题1:
假设目前有界面A,需要跳转到界面B,但界面B必须经过用户实名身份认证之后才能进入,如果没有实名认证,那么只能跳转到C,而界面C也需要用户登录成功后才能进入,否则就需要先跳转到登录界面D。
这怎么办?当然,可以在A界面就判断是否登录,是否进行了实名认证等等,然后分别跳转到不同界面。
唔。。。,这个确实比较麻烦,如果界面足够多,光是其中的逻辑就得炸掉。
在考虑一个问题2:
软件开发时分了多个模块,比如有X和Y;X中有活动A需要向Y模块中的活动B跳转,可惜的是,X模块开发者只知道活动B名字,但需要传什么值就懵了,如果此时有模块Z,同时有活动C需要向活动B跳转,且跳转时可能携带不同参数。。。
可以想象,Y模块的开发者在编写活动B时是多么痛苦,需要同时兼顾多人的使用。
又是一个框架,先看一下对于跳转参数的处理:
/**
* 非必须字段,可以为null,表示使用关键字进行搜索过滤
*/
@Autowired(name = KEY_FILTER_SOURCE, required = false)
List source;
就这样,一个参数,一个解释说明,说明什么情况下需要传递,required表示是否必须,name则说明KEY值。
注:这里需要说明,required字段设置为true或者false对程序执行不会有影响,即便required为true的字段没有传入,ARouter框架也是不会自动让其Crash掉的
然后再说明一下ARouter对于权限的拦截处理;
相对于之前所说的“笨方法”——在当前界面判断所有的权限来进行跳转;
Arouter取了个巧,让目标界面自己声明自己所需的权限.
比如上面的问题1:让B界面字节声明自己需要实名认证权限,如果不满足,在拦截器中进行降级,主动前往C界面,同样的在C界面声明了登录权限,在拦截器中也做出了处理:如果未登录,降级跳转到登录界面。
如此依赖,界面A中只要发出自己想要向C界面跳转的请求,能不能成功,就不需要自己来处理了。
具体介绍见:路由拦截模块——ARouter
持续更新中。。。