[Android] gradient中设置angle角度问题

在设置gradient时,经常需要设置角度,android:angle=”90″,比如:

<shape
xmlns:android=”http://schemas.android.com/apk/res/android”
android:shape=”rectangle”>
<gradient
android:startColor=”@color/mainPinkStart”
android:endColor=”@color/mainPinkEnd”
android:angle=”90″ />
</shape>

这里需要注意的是,angle值只能是45的倍数,否则会出现下面的错误log:

Caused by: org.xmlpull.v1.XmlPullParserException: Binary XML file line #5<gradient> tag requires ‘angle’ attribute to be a multiple of 45
at android.graphics.drawable.GradientDrawable.updateGradientDrawableGradient(GradientDrawable.java:1354)
at android.graphics.drawable.GradientDrawable.inflateChildElements(GradientDrawable.java:1176)

[Android] FragmentPagerAdapter or FragmentStatePageAdapter获取Fragment

ViewPager和Fragment搭配使用,是很多APP经常采用的方法,但其中有很多“坑”要注意。比如这里有个需求,需要获取某个Fragment的实例,来进行一些操作。这里又分FragmentPagerAdapter 和 FragmentStatePageAdapter,两者区别见

1.对于FragmentPagerAdapter,经常用的是

"android:switcher:" + viewId + ":" + position

2.对于FragmentStatePageAdapter,不支持上面方式,这里有个简单的办法:

myFragmentStatePageAdpater.instantiateItem(null, position)

参考:http://stackoverflow.com/questions/12384971/android-fragmentstatepageradapter-how-to-tag-a-fragment-to-find-it-later

http://stackoverflow.com/questions/14035090/how-to-get-existing-fragments-when-using-fragmentpageradapter#

http://stackoverflow.com/questions/8785221/retrieve-a-fragment-from-a-viewpager

[Android] FragmentPagerAdapter 和 FragmentStatePageAdapter区别

一、官方解释

FragmentPagerAdapter 

This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, consider FragmentStatePagerAdapter.

FragmentStatePageAdapter

This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to FragmentPagerAdapter at the cost of potentially more overhead when switching between pages.

二、通俗解释

1. 适用对象:

FragmentPagerAdapter适用static and less count数目Fragment

FragmentStatePageAdapter适用dynamic and more count数目Fragment

2. Fragment生命周期

FragmentPagerAdapter中的Fragment是Detached,即,仅仅是对应的View被回收,整个Fragment实例仍然存在。

FragmentStatePageAdapter中的Fragment是Removed/Destroyed,整个Fragment实例都被回收干掉。

3. 内存占用

FragmentPagerAdapter占用内存大,因为Fragment实例仍然存在。

FragmentStatePageAdapter不占用内存,但它并不是永远消失了,从名字中的State可以看出,系统会把Fragment的state作为Bundle通过savedInstanceState而保存下来,当我们重新回到该Fragment后,可以从onViewStateRestored中恢复。

参考:http://stackoverflow.com/questions/30235335/difference-between-fragmentpageradapter-with-viewpager-with-offscreenlimit-set-t

[Android] TextView中setPadding()方法失效,不起作用

项目中使用了一个第三方控件,很多人都有用过,PagerSlidingTabStrip,发现在4.4(Kitkat)之前的版本,每个tab会挤在一起,分析源码后,发现每个tab在设置了setPadding后根本就无效,网上一查,还真有坑。

原因是在每个tab调用了setPadding后,又在其他地方调用了setBackgroundResource(),这在4.4之前是有问题的,4.4及以后的版本google已经fix了这个bug。

// does not work
tv.setPadding(20, 20, 20, 20);
tv.setBackgroundResource(R.drawable.border);

// works
tv.setBackgroundResource(R.drawable.border);
tv.setPadding(20, 20, 20, 20);

所以,解决办法是,必须在setBackgroundResource之后调用setPadding()方法。

[Android]Service可以显示对话框?

嗯,总有一些莫名其妙的需求,不过,仔细一想,这种情况还是存在的,比如手机电量低时,会弹出一个警告对话框,还有闹钟也会弹对话框,好,那就看看怎么来实现吧。

1.让service启动一个activity,该activity实际上是一个dialog类型,设置theme如下:

android:theme="@android:style/Theme.Dialog"

有个开源项目android-smspopup就是用的这个方法。

2.不使用activity

AlertDialog alertDialog = new AlertDialog.Builder(this)
                    .setTitle("Title")
                    .setMessage("Are you sure?")
                    .create();

alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
alertDialog.show();

在manifest里要用到一个权限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

3. 基本上以上两个办法就可以了,更详细的讨论可以参考下这篇文章

 

Android自定义View实现左右滑动选择出生年份

转自:http://www.jb51.net/article/86071.htm

模仿的是微博运动界面的个人出生日期设置view,先看看我的效果图:

支持设置初始年份,左右滑动选择出生年份,对应的TextView的值也会改变。这个动画效果弄了好久,感觉还是比较生硬,与微博那个还是有点区别。大家有改进的方案,欢迎一起交流。

自定义View四部曲,这里依旧是这个套路,看看怎么实现的。

1.自定义view的属性:
在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性以及声明我们的整个样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<resources>
 //自定义属性名,定义公共属性
 <attr name="titleSize" format="dimension"></attr>
 <attr name="titleText" format="string"></attr>
 <attr name="titleColor" format="color"></attr>
 <attr name="outCircleColor" format="color"></attr>
 <attr name="inCircleColor" format="color"></attr>
 <attr name="lineColor" format="color"></attr>
 <declare-styleable name="MyScrollView">
  <attr name="titleSize"></attr>
  <attr name="titleColor"></attr>
  <attr name="lineColor"></attr>
 </declare-styleable>
</resources>

依次定义了字体大小,字体颜色,线的颜色3个属性,format是值该属性的取值类型。
然后就是在布局文件中申明我们的自定义view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<TextView
 android:id="@+id/year_txt"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_margin="30dp"
 android:text="出生年份 (年)"
 android:textSize="20dp" />
<com.example.tangyangkai.myview.MyScrollView
 android:id="@+id/scroll_view"
 android:layout_width="match_parent"
 android:layout_height="70dp"
 myscroll:lineColor="@color/font_text"
 myscroll:titleColor="@color/strong"
 myscroll:titleSize="30dp">
</com.example.tangyangkai.myview.MyScrollView>

自定义view的属性我们可以自己进行设置,记得最后要引入我们的命名空间,
xmlns:app=”http://schemas.Android.com/apk/res-auto”

2.获取自定义view的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public MyScrollView(Context context) {
 this(context, null);
}
public MyScrollView(Context context, AttributeSet attrs) {
 this(context, attrs, 0);
}
public MyScrollView(final Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 //获取我们自定义的样式属性
 TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyScrollView, defStyleAttr, 0);
 int n = array.getIndexCount();
 for (int i = 0; i < n; i++) {
  int attr = array.getIndex(i);
  switch (attr) {
   case R.styleable.MyScrollView_lineColor:
    // 默认颜色设置为黑色
    lineColor = array.getColor(attr, Color.BLACK);
    break;
   case R.styleable.MyScrollView_titleColor:
    textColor = array.getColor(attr, Color.BLACK);
    break;
   case R.styleable.MyScrollView_titleSize:
    // 默认设置为16sp,TypeValue也可以把sp转化为px
    textSize = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
      TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
    break;
  }
 }
 array.recycle();
 init();
}
private void init() {
 //初始化
 mPaint = new Paint();
 mPaint.setAntiAlias(true);
 mBound = new Rect();
 mTxtBound = new Rect();
 bigTxtSize = textSize;
 oneSize = textSize - 15;
 thirdSize = textSize - 15;
}

自定义View一般需要实现一下三个构造方法,这三个构造方法是一层调用一层的,属于递进关系。因此,我们只需要在最后一个构造方法中来获得View的属性以及进行一些必要的初始化操作。尽量不要在onDraw的过程中去实例化对象,因为这是一个频繁重复执行的过程,new是需要分配内存空间的,如果在一个频繁重复的过程中去大量地new对象会造成内存浪费的情况。

3.重写onMesure方法确定view大小:

上一篇自定义View的文章介绍的很详细,这里就不重复了,重点放在onDraw方法里面:
Android自定义View仿微博运动积分动画效果

4.重写onDraw方法进行绘画:

之前说过对于比较复杂的自定义View,重写onDraw方法之前,首先在草稿本上将大致的样子画出来,坐标,起始点都可以简单标注一下。这个方法很实用,思路很清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
 int action = ev.getAction();
 int x = (int) ev.getX();
 int y = (int) ev.getY();
 switch (action) {
  case MotionEvent.ACTION_DOWN:
   xDown = x;
   yDown = y;
   break;
  case MotionEvent.ACTION_MOVE:
   xMove = x;
   yMove = y;
   dx = xMove - xDown;
   int dy = yMove - yDown;
   //如果是从左向右滑动
   if (xMove > xDown && Math.abs(dx) > mTouchSlop * 2 && Math.abs(dy) < mTouchSlop) {
    state = 1;
   }
   //如果是从右向左滑动
   if (xMove < xDown && Math.abs(dx) > mTouchSlop * 2 && Math.abs(dy) < mTouchSlop) {
    state = 2;
   }
   break;
  case MotionEvent.ACTION_UP:
   break;
 }
 return super.dispatchTouchEvent(ev);
}

重写View的dispatchTouchEvent方法来区别左右滑动,mTouchSlop是Android默认的滑动最小距离,如果水平方向滑动的距离大于竖直方向滑动的距离,就判断为水平滑动。这里为了不让滑动那么明显,我让水平滑动的距离大于默认距离的两倍才判定左右滑动。state是记录滑动的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Override
public boolean onTouchEvent(MotionEvent ev) {
 int action = ev.getAction();
 switch (action) {
  case MotionEvent.ACTION_DOWN:
   break;
  case MotionEvent.ACTION_MOVE:
   if (state == 1 && bigTxtSize - oneSize > -15) {
    bigTxtSize = bigTxtSize - 1;
    oneSize = oneSize + 1;
    postInvalidate();
   }
   if (state == 2 && bigTxtSize - thirdSize > -15) {
    bigTxtSize = bigTxtSize - 1;
    thirdSize = thirdSize + 1;
    postInvalidate();
   }
   break;
  case MotionEvent.ACTION_UP:
   if (state == 1) {
    size = size - 1;
    bigTxtSize = textSize;
    oneSize = textSize - 15;
    postInvalidate();
    listener.OnScroll(size);
    state = 0;
   }
   if (state == 2) {
    size = size + 1;
    bigTxtSize = textSize;
    thirdSize = textSize - 15;
    postInvalidate();
    listener.OnScroll(size);
    state = 0;
   }
   break;
 }
 return true;
}

重写View的onTouchEvent方法来处理View的点击事件。
(1)演示动态图中,左右滑动的过程中,中间数字会从大变小,左右的数字会从小变大,bigTxtSize代表中间的数字大小,oneSize代表从左到右第二个数字的大小,thirdSize代表从左到右第四个数字的大小。在滑动过程中再使用postInvalidate()方法来一直调用onDraw方法来重新进行绘制,达到数字大小变化的效果。
(2)滑动结束以后进行判断,如果是从左向右滑动,就会将数字减一;如果是从右向左滑动,就会将数字加一。最后将数字大小,滑动状态恢复到默认值。
(3)最后一定要返回true,表示消费当前滑动事件,不然滑动没反应

滑动的操作已经全部处理好,接下来就是绘制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Override
protected void onDraw(Canvas canvas) {
 txtSize = size - 2;
 bigText = String.valueOf(size);
 smallText = String.valueOf(txtSize);
 mPaint.setColor(lineColor);
 canvas.drawLine(0, 0, getWidth(), 0, mPaint);
 canvas.drawLine(0, getHeight(), getWidth(), getHeight(), mPaint);
 lineX = getWidth() / 10;
 for (int i = 0; i < 5; i++) {
  if (i == 2) {
   mPaint.setTextSize(bigTxtSize);
   if (bigTxtSize == textSize - 15) {
    mPaint.setColor(lineColor);
    canvas.drawLine(lineX, 0, lineX, getHeight() / 5, mPaint);
   } else {
    mPaint.setColor(textColor);
    canvas.drawLine(lineX, 0, lineX, getHeight() / 3, mPaint);
   }
   mPaint.getTextBounds(bigText, 0, bigText.length(), mBound);
   canvas.drawText(bigText, lineX - mBound.width() / 2, getHeight() / 2 + mBound.height() * 3 / 4, mPaint);
  } else if (i == 0 || i == 4) {
   mPaint.setColor(lineColor);
   mPaint.setTextSize(textSize - 15);
   mPaint.getTextBounds(smallText, 0, smallText.length(), mTxtBound);
   canvas.drawLine(lineX, 0, lineX, getHeight() / 5, mPaint);
   canvas.drawText(String.valueOf(txtSize), lineX - mTxtBound.width() / 2, getHeight() / 2 + mTxtBound.height() * 3 / 4, mPaint);
  } else if (i == 1) {
   mPaint.setTextSize(oneSize);
   if (oneSize == textSize) {
    mPaint.setColor(textColor);
    canvas.drawLine(lineX, 0, lineX, getHeight() / 3, mPaint);
   } else {
    mPaint.setColor(lineColor);
    canvas.drawLine(lineX, 0, lineX, getHeight() / 5, mPaint);
   }
   mPaint.getTextBounds(smallText, 0, smallText.length(), mTxtBound);
   canvas.drawText(String.valueOf(txtSize), lineX - mTxtBound.width() / 2, getHeight() / 2 + mTxtBound.height() * 3 / 4, mPaint);
  } else {
   mPaint.setTextSize(thirdSize);
   if (thirdSize == textSize) {
    mPaint.setColor(textColor);
    canvas.drawLine(lineX, 0, lineX, getHeight() / 3, mPaint);
   } else {
    mPaint.setColor(lineColor);
    canvas.drawLine(lineX, 0, lineX, getHeight() / 5, mPaint);
   }
   mPaint.getTextBounds(smallText, 0, smallText.length(), mTxtBound);
   canvas.drawText(String.valueOf(txtSize), lineX - mTxtBound.width() / 2, getHeight() / 2 + mTxtBound.height() * 3 / 4, mPaint);
  }
  txtSize++;
  lineX += getWidth() / 5;
 }
}

这里其实就是得到滑动操作的数字尺寸大小,然后进行绘制,最后将数字每次加一,lineX是B点的初始位置,每次加上宽度的五分之一。

5.得到当前的设置值
可以看到View上面的TextView也会跟着下面设置的值改变,所以这里我们需要单独处理一下。接口回调,简单暴力的方式。

在onTouchEvent的case MotionEvent.ACTION_UP中,得到最后设置的值

listener.OnScroll(size);

然后就是对应的Activity了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SecondActivity extends AppCompatActivity implements MyScrollView.OnScrollListener {
 private MyScrollView scrollView;
 private TextView txt;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_second);
  initview();
 }
 private void initview() {
  scrollView = (MyScrollView) findViewById(R.id.scroll_view);
  scrollView.setSize(1992);
  scrollView.setListener(this);
  txt = (TextView) findViewById(R.id.year_txt);
  txt.setText("出生年份" + scrollView.getSize() + " (年)");
 }
 @Override
 public void OnScroll(int size) {
  txt.setText("出生年份" + size + " (年)");
 }
}

实现接口的方法,进行初始化,设置初始值,然后就是在接口的方法更新数据即可。

[Android] 一个全能型Android开发者需要掌握的知识

从09年开始做Android已经7年了,这期间Android的发展和变化是很大的,特别是第三方库很多,而且已经形成了体系,个人要开发一个小的app比以前容易多了,直接调用别人写的好的库,再自己学着搭搭框架,很快一个app就完成了,但如果要想深入理解其中的原理,还是要认真的分析源码,不管是Android的还是第三方库的,最好理解其中的原理。

下面总结了下要开发一个“像样”的App,大概需要掌握的知识,后面还会更新:

%e5%bc%80%e5%8f%91%e6%b5%81%e7%a8%8b

 

 

[Andorid]有赞Android客户端网络架构演进

转自:有赞技术团队博客 http://tech.youzan.com/android_http/

Android客户端网络请求是每一个应用都不可或缺的模块,其设计的好坏直接影响应用的性能和代码稳定性、扩展性。Android网络请求最开始官方只提供了最基础的方法,开发者必须在此基础上进行二次封装,这样就要求开发者对Http请求协议、缓存、JSON转换、错误处理以及线程切换等都比较熟悉,稳定性、可扩展性和可维护性都是比较大的挑战。

目前Android主流的网络请求都是基于Square公司开发的OkHttp,该框架也得到了Google官方的认可,OkHttp对网络请求做了大量的封装和优化,极大降低了开发者的使用成本,同时兼备稳定性和可扩展性。目前有赞Android客户端也是采用OkHttp进行网络请求,在OkHttp框架的基础上做了大量的封装和优化,减小业务逻辑与框架的耦合性的同时,也极大降低了业务方的使用成本。

1. 现在的网络请求

先以Get请求为例,代码如下:

private static final String TRADE_CATEGORIES = "kdt/tradecategories/get";  
/**
 * 获取订单类型
 *
 * @param context context
 * @param callback TradeCategoryEntity callback
 */
public void requestTradeCategories(Context context,Map<String, String> params, BaseTaskCallback<List<TradeCategoryEntity>> callback) {  
    RequestApi api = createRequestTokenApi(TRADE_CATEGORIES);
    if (null != params) {
        api.addMultipleRequestParams(params);
    }
    api.setMethod(RequestApi.Method.GET);
    api.setResponseKeys(GoodsTradeApi.ResponseKey.RESPONSE, "categories");
    doRequest(context, api, true, callback, TaskErrorNoticeMode.NONE);
}

针对上述Get请求需要几点说明:

  1. 每个模块都需要一个单独的网络请求类,罗列该模块所有的业务请求方法
  2. 请求url需要在每个请求类中定义,如上文中的TRADE_CATEGORIES
  3. 每次请求都需要调用createRequestTokenApi方法,完成token参数的封装
  4. 请求参数可以直接通过addRequestParams添加,也可以以Map的形式统一添加addMultipleRequestParams
  5. 请求方法通过setMethod方法设置
  6. 返回值可以通过setResponseKeys指定需要的具体JSON节点
  7. 请求结果通过BaseTaskCallback回调,返回值类型也可通过泛型方式指定
  8. 回调方法包括请求成功、请求失败以及请求时机等方法

以下是请求结果回调方法代码:

// 请求开始的回调方法
public void onBefore(RequestApi api) {

}
// 请求结束的回调方法
public void onAfter() {

}
// 请求成功的回调方法,data表示返回值,statusCode表示请求状态值
public void onResponse(T data, int statusCode){

}
// 网络请求失败的回调方法
public void onError(ErrorResponse errorResponse) {

}
// 请求的业务错误回调方法
public void onRequestError(ErrorResponse errorResponse) {

}

业务方只需要在不同的回调方法中处理不同的业务即可,比如说在onBefore中显示进度条,在onAfter中隐藏进度条,在onResponse中获取返回值以及状态值,onRequestError中获取错误类型,以及显示错误提示等等。

针对上述请求过程,可以发现还是有一些不是很合理的地方。

  1. 每次新建一个网络接口方法都需要设置请求url、封装token、设置请求方法等,造成了大量的重复代码
  2. 请求url和具体的请求方法分开定义,不够直观
  3. 如果请求参数比较多的时候,使用Map方式容易写错,而且在编译过程和自测阶段不会有错误提示
  4. 请求成功回调方法的线程切换还是需要借助Handler等方式实现

2. 改进后的网络请求框架

针对以上问题,我们引入Square公司开发的Retrofit和ReactiveX出品的RxJava,以下是结合有赞业务后网络请求用法。

首先定义网络请求方法,Retrofit是通过接口的方式完成请求方法定义的,代码如下:

public interface TradesService {  
    @GET("api.tradecategories/1.0.0/get")
    Observable<Response<CategoryResponse>> tradeCategories();

    @FormUrlEncoded
    @GET("api.trade/1.0.0/get")
    Observable<Response<TradeItemResponse>> tradeDetail(@Field("tid") String tid);
}

通过注解的方式指定请求类型、请求url、请求参数,返回值是RxJava的Observable对象,就可以进行一系列的链式操作,具体用法等一下会详细讲述。通过对比两种请求方法的写法,很显然,通过Retrofit定义请求方法更简洁清晰,下面我们来看一下如何通过RxJava实现错误信息的统一处理。

2.1 请求错误处理

首先定义一个BaseResponse,所有的Response都要继承自它。

public class BaseResponse {  
    public static final int CODE_SUCCESS = 0;

    public String msg;
    public int code;
    @SerializedName("error_response")
    public ErrorResponse errorResponse;

    public static final class ErrorResponse {
        public String msg;
        public int code;
    }
}

BaseResponse定义了错误信息,后续所有的Response都会继承BaseResponse,而服务端返回的错误信息都会进行集中处理,处理逻辑代码如下:

 // 下文的T:T extends Response<R>, R: R extends BaseResponse
 public Observable<R> call(Observable<T> observable) {
    return observable.map(new Func1<T, R>() {
        @Override
        public R call(T t) {
            String msg = null;
            if (!t.isSuccessful() || t.body() == null) {
                msg = mDefaultErrorMsg;
            } else if (t.body().code != BaseResponse.CODE_SUCCESS) {
                msg = t.body().msg;
                if (msg == null) {
                    msg = mDefaultErrorMsg;
                }
                tryLogin(t.body().code);
            } else if (t.body().errorResponse != null) {
                msg = t.body().errorResponse.msg;
                if (msg == null) {
                    msg = mDefaultErrorMsg;
                }
                tryLogin(t.body().errorResponse.code);
            }
            if (msg != null) {
                try {
                    throw new ErrorResponseException(msg);
                } catch (ErrorResponseException e) {
                    throw Exceptions.propagate(e);
                }
            }
            return t.body();
        }
    });     

关于上面的代码有几点需要说明:

  1. T继承了Retrofit提供的Response类,该类包含了Http返回值、协议版本号等等通用信息,R是具体的业务数据结构,如同上文所说,R继承了BaseResponse,方便统一处理错误信息。
  2. 业务的错误返回值都是事先定义好的,只需要根据返回的错误码和错误类型进行处理即可
  3. 这里的错误类型本质上是利用RxJava的map转换方法,即将一种Observable转换成另一种Observable,转换的过程中对不同的错误类型进行处理,同时将处理后的结果通过Observable传递出去

2.2 线程切换

正如上文所言,之前的请求方法在请求回调方法中无法方便的切换线程,必须要借助Android原生的Handler方式,代码就会比较分散,可读性也比较差,而RxJava针对线程切换提供了很友好的处理方法,只需要显式的设置即可完成不同线程的切换。

public class SchedulerTransformer<T> implements Observable.Transformer<T, T> {  
    @Override
    public Observable<T> call(Observable<T> observable) {
        return observable
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
}

上面的代码主要完成了两部分的工作,subscribeOn声明生产者Observable所在的线程,由于网络请求比较耗时,一般会放到IO线程或者单独的子线程;而observeOn声明了处理请求返回值所在的线程为Android系统的主线程,也即UI线程,这样就可以在处理返回值时更新UI。

2.3 请求日志

网络请求经常会出现各种各样的异常情况,这个时候就需要通过查看请求日志来跟踪定位问题,OkHttp提供了打印完整日志的方法,方便开发者调试网络请求。OkHttp官方提供了一个很方便查看日志的Interceptor,你可以控制你需要的打印信息类型,使用方法也很简单。

首先需要在build.gradle文件中引入logging-interceptor

compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'  

在OkHttpClient创建处添加创建好的Interceptor即可,完整的示例代码如下:

private static OkHttpClient getNewClient(){  
    HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
    logging.setLevel(HttpLoggingInterceptor.Level.BODY);
    return new OkHttpClient.Builder()
           .addInterceptor(logging)
           .build();
}

HttpLoggingInterceptor提供了4中控制打印信息类型的等级,分别是NONE,BASIC,HEADERS,BODY,接下来分别来说一下相应的打印信息类型。

  • NONE

    没有任何日志信息

  • Basic

    打印请求类型,URL,请求体大小,返回值状态以及返回值的大小

  D/HttpLoggingInterceptor$Logger: --> POST /upload HTTP/1.1 (277-byte body)  
  D/HttpLoggingInterceptor$Logger: <-- HTTP/1.1 200 OK (543ms, -1-byte body)  
  • Headers

    打印返回请求和返回值的头部信息,请求类型,URL以及返回值状态码

  <-- 200 OK https://your_url (3787ms)
  D/OkHttp: Date: Sat, 06 Aug 2016 14:26:03 GMT
  D/OkHttp: Content-Type: application/json; charset=utf-8
  D/OkHttp: Transfer-Encoding: chunked
  D/OkHttp: Connection: keep-alive
  D/OkHttp: Keep-Alive: timeout=30
  D/OkHttp: Vary: Accept-Encoding
  D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
  D/OkHttp: Pragma: no-cache
  D/OkHttp: Cache-Control: must-revalidate, no-cache, private
  D/OkHttp: Set-Cookie: bid=D6UtQR5N9I4; Expires=Sun, 06-Aug-17 14:26:03 GMT; Domain=.douban.com; Path=/
  D/OkHttp: X-DOUBAN-NEWBID: D6UtQR5N9I4
  D/OkHttp: X-DAE-Node: dis17
  D/OkHttp: X-DAE-App: book
  D/OkHttp: Server: dae
  D/OkHttp: <-- END HTTP
  • Body

    打印请求和返回值的头部和body信息

  <-- 200 OK https://your_url (3583ms)
  D/OkHttp: Connection: keep-alive
  D/OkHttp: Date: Sat, 06 Aug 2016 14:29:11 GMT
  D/OkHttp: Keep-Alive: timeout=30
  D/OkHttp: Content-Type: application/json; charset=utf-8
  D/OkHttp: Vary: Accept-Encoding
  D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
  D/OkHttp: Transfer-Encoding: chunked
  D/OkHttp: Pragma: no-cache
  D/OkHttp: Connection: keep-alive
  D/OkHttp: Cache-Control: must-revalidate, no-cache, private
  D/OkHttp: Keep-Alive: timeout=30
  D/OkHttp: Set-Cookie: bid=ESnahto1_Os; Expires=Sun, 06-Aug-17 14:29:11 GMT; Domain=.douban.com; Path=/
  D/OkHttp: Vary: Accept-Encoding
  D/OkHttp: X-DOUBAN-NEWBID: ESnahto1_Os
  D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
  D/OkHttp: X-DAE-Node: dis5
  D/OkHttp: Pragma: no-cache
  D/OkHttp: X-DAE-App: book
  D/OkHttp: Cache-Control: must-revalidate, no-cache, private
  D/OkHttp: Server: dae
  D/OkHttp: Set-Cookie: bid=5qefVyUZ3KU; Expires=Sun, 06-Aug-17 14:29:11 GMT; Domain=.douban.com; Path=/
  D/OkHttp: X-DOUBAN-NEWBID: 5qefVyUZ3KU
  D/OkHttp: X-DAE-Node: dis17
  D/OkHttp: X-DAE-App: book
  D/OkHttp: Server: dae
  D/OkHttp: {"count":3,"start":0,"total":778,"books":[{"rating":{"max":10,"numRaters":202900,"average":"9.0","min":0},"subtitle":"","author":["[法] 圣埃克苏佩里"],"pubdate":"2003-8","tags":[{"count":49322,"name":"小王子","title":"小王子"},{"count":41381,"name":"童话","title":"童话"},{"count":19773,"name":"圣埃克苏佩里","title":"圣埃克苏佩里"}
  D/OkHttp: <-- END HTTP (13758-byte body)

2.3 统一添加token和User-Agent

现在基本上所有的应用都会通过token来鉴定用户权限,User-Agent参数方便服务端获取更多手机本地的信息,类似这样的每一个请求都需要的参数,也可以通过Interceptor方式实现。

public class LoginInterceptor implements Interceptor {  
    public static final String ACCESS_TOKEN = "access_token";
    public static final String USER_AGENT = "User-Agent";

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        Request request = original.newBuilder()
                .header(USER_AGENT, AppUtils.getUserAgent() + " app_name/" + AppUtils.getVersionName())
                .header(ACCESS_TOKEN, AppUtils.getToken())
                .method(original.method(), original.body())
                .build();
        return chain.proceed(request);
    }
}

2.4 通过对象封装请求体

有的时候post请求的参数会有很多个,如果一个个写,那么方法的参数就显得很多,杂乱且不好维护,当然可以用Map的方式集中管理,但是Map有一个问题就是一旦key值写错了,前期是无法及时发现的,必须等到真正的网络请求的时候才能发现。Retrofit提供了一个很好的解决方法,通过对象封装这些请求参数。

@FormUrlEncoded
@POST("book/reviews")
Observable<Response<String>> addReviews(@Body Reviews reviews);

public class Reviews {  
    public String book;
    public String title;
    public String content;
    public String rating;
}

需要注意的是类中的属性名必须要和请求参数的key保持一致,否则服务端无法正常识别请求参数

关于Retrofit详细的用法可以参考文章Retrofit详解

3. 如何使用

我们以获取Category为例来说明如何利用RetrofitRxAndroid来改写现有模块。

3.1 定义CategoryResponse

CategoryResponse必须继承自BaseResponse,里面包含了错误信息的数据结构。

@Keep
public class CategoryResponse extends BaseResponse {  
    public Response response;

    @Keep
    public static final class Response {
        public List<Category> categories;
    }
}

其中Category是具体的实体类型。

3.2 定义Service Method

public interface TradesService {  
    @GET("api/tradecategories/get")
    Observable<Response<CategoryResponse>> tradeCategories();
}

注意点

  • TradesService必须是一个interface,而且不能继承其他interface
  • tradeCategories的返回值必须是Observable类型。

3.3 利用ServiceFactory创建一个TradeService实例

在适当的时机(Activity#onCreateFragment#onViewCreated等)根据网关类型通过ServiceFactory创建一个TradeService实例。

mTradesService = ServiceFactory.createNewService(TradesService.class)  

3.4 TradeService获取数据

mTradesService.tradeCategories()  
        .compose(new DefaultTransformer<CategoryResponse>(getActivity()))
        .map(new Func1<CategoryResponse, List<Category>>() {
            @Override
            public List<Category> call(CategoryResponse response) {
                return response.response.categories;
            }
        })
        .flatMap(new Func1<List<Category>, Observable<Category>>() {
            @Override
            public Observable<Category> call(List<Category> categories) {
                return Observable.from(categories);
            }
        })
        .subscribe(new ToastSubscriber<Category>(getActivity) {
            @Override
            public void onCompleted() {
                hideProgressBar();
                // business related code
            }

            @Override
            public void onError(Throwable e) {
                super.onError(e);
                hideProgressBar();
                // business related code
            }

            @Override
            public void onNext(Category category) {
                // business related code
            }
        });

注意:DefaultTransformer包含了线程分配错误处理两部分功能,所以调用方只需要关心正确的数据就可以了。

DefaultTransformer包含了上文提到的线程切换转换器SchedulerTransformer和错误处理转换器ErrorCheckerTransformer,具体代码如下:

public class DefaultTransformer<R extends BaseResponse>  
        implements Observable.Transformer<Response<R>, R> {

    private Context context;

    public DefaultTransformer(final Context context) {
        this.context = context;
    }

    @Override
    public Observable<R> call(Observable<Response<R>> observable) {
        return observable
                .compose(new SchedulerTransformer<Response<R>>())
                .compose(new ErrorCheckerTransformer<Response<R>, R>(context));
    }
}

4.写在最后

网络请求对于提升Android应用的体验和性能有很大的影响,结合Retrofit使用提高了代码可读性和编码的灵活性,RxJava提供了链式调用方式,融合了线程切换、数据过滤、数据转换等优点。有赞在此基础上进行了少量的封装,便可适应大部分复杂的业务场景。

[Android] apk打包问题

多次在生产签名打包后的apk,出现功能不可用的情况,比方说有个社会化分享功能,写代码时都可以正常实现,但签名生成apk后该功能无法再使用了,点击分享面板的平台,没有任何响应。请问是怎么回事,这种问题解决应该从哪几个方面入手,希望有一些思路可供参考

  • 应该是混淆引起的
  • 混淆是将易读性较好的变量,方法和类名替换成可读性较差的名称
  • 混淆的目的是为了加大逆向的成本,但不能避免
  • 通常混淆的处理是将某些库不加入混淆
  • 第三方的库不建议混淆

一些需要排除混淆的

  • 被native方法调用的java方法
  • 供javascript调用的java方法
  • 反射调用的方法
  • AndroidManifest中声明的组件
  • 总结:即所有硬编码的元素(变量,方法,类)

关于混淆,请参考文章读懂 Android 中的代码混淆

[Android] Bitmap优化

Bitmap优化

  • options.inJustDecodeBounds = true;可以获取width,height和mimetype等信息,但不会申请内存占用
  • 合理进行缩放,一个高分辨率的图片不仅展示在一个小的imageView中,不仅不会有任何视觉优势,反而还占用了很大的内存
  • 将Bitmap处理移除主线程
  • 使用LruCache或者DiskLruCache缓存Bitmap
  • before 2.3 手动调用recycle()方法

关于Bitmap的文章