Android 7.0 App内切换语言不生效的问题

Android6.0及以前App内语言切换

Android6.0及以前版本,Configuration中的语言相当于是App的全局设置

    public static void changeAppLanguage(Context context, String newLanguage){
        Resources resources = context.getResources();
        Configuration configuration = resources.getConfiguration();

        // app locale
        Locale locale = getLocaleByLanguage(newLanguage);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            configuration.setLocale(locale);
        } else {
            configuration.locale = locale;
        }

        // updateConfiguration
        DisplayMetrics dm = resources.getDisplayMetrics();
        resources.updateConfiguration(configuration, dm);
    }
在App启动和设置改变的时候,更新语言
public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        onLanguageChange();
    }

    /**
     * Handling Configuration Changes
     * @param newConfig newConfig
     */
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        onLanguageChange();
    }

    private void onLanguageChange() {
        String language;//读取App配置
        AppLanguageUtils.changeAppLanguage(this, language);
    }
}

Android7.0及之后App内语言切换

Android7.0及之后版本,使用了LocaleList,Configuration中的语言设置可能获取的不同,而是生效于各自的Context。

这会导致:Android7.0使用就的方式,有些Activity可能会显示为手机的系统语言。

Android7.0 优化了对多语言的支持

废弃了updateConfiguration()方法,替代方法:createConfigurationContext(),这尼玛返回的是Context。

也就是语言需要植入到Context中,每个Context都植入一遍,有点恶心。

    @Deprecated
    public void updateConfiguration(Configuration config, DisplayMetrics metrics) {
        updateConfiguration(config, metrics, null);
    }


    @Override
    public Context createConfigurationContext(Configuration overrideConfiguration) {
        return mBase.createConfigurationContext(overrideConfiguration);
    }
Android7.0及之后通过Context来设置语言

不能必现,但是如果使用新的方法,那么所有的Context都需要设置(包括Application),并且设置:configuration.setLocales(new LocaleList(locale));

    private void onChangeAppLanguage(String newLanguage) {
        AppLanguageUtils.changeAppLanguage(getActivity(), newLanguage);
        AppLanguageUtils.changeAppLanguage(App.getContext(), newLanguage);
        getActivity().recreate();
    }

    // Activity and Application
    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(AppLanguageUtils.attachBaseContext(newBase, language));
    }

    // AppLanguageUtils.java
    public static Context attachBaseContext(Context context, String language) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return updateResources(context, language);
        } else {
            return context;
        }
    }


    @TargetApi(Build.VERSION_CODES.N)
    private static Context updateResources(Context context, String language) {
        Resources resources = context.getResources();
        Locale locale = AppLanguageUtils.getLocaleByLanguage(language);

	Configuration configuration = resources.getConfiguration();
	configuration.setLocale(locale);
	configuration.setLocales(new LocaleList(locale));
	return context.createConfigurationContext(configuration);
    }

参考文献

GitHub地址

MultiLanguagesSwitch

搞清日期、时间与时区

涉及到跨时区的业务,需要搞清楚几个时间相关的概念:

格林尼治标准时间:Greenwich Mean Time(简称 GMT)

指位于伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线。
理论上来说,格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时的时间。
但由于地球在它的椭圆轨道里的运动速度不均匀,这个时刻可能和实际的太阳时相差16分钟。
由于地球每天的自转是有些不规则的,而且正在缓慢减速。所以,格林尼治时间已经不再被作为标准时间使用。
现在的标准时间(UTC)由原子钟提供。(在计算机中GMT 和 UTC 仍是等同的)

协调世界时(UTC):

又称世界标准时间或世界协调时间,是当今最主要的世界时间标准,以原子时秒长为基础。
国际原子时的误差为每日数纳秒,世界时的误差为每日数毫秒,UTC 便是这两种时标的一种折中。
为确保 UTC 与世界时相差不会超过 0.9 秒,在有需要的情况下会在协调世界时内加上正或负闰秒。因此协调世界时与国际原子时之间会出现若干整数秒的差别。位于巴黎的国际地球自转事务中央局负责决定何时加入闰秒,一般会在每年的 6 月 30 日、12 月 31 日的最后一秒进行调整。
UTC 的应用及其广泛,被应用在大多数的计算机以及网络标准中。

夏令时与冬令时 Daylight Saving Time(简称 DST)

又称“日光节约时制”和“夏令时间”,是一种为节约能源而人为规定地方时间的制度,在这一制度实行期间所采用的统一时间称为“夏令时间”。
一般在天亮早的夏季人为将时间提前一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。。

多时区处理

在实际开发中,当时间用于显示时,一般使用系统默认的时区时间作为显示时间。将时间做为数据存储或传递给其他系统时(特别是跨平台调用),则使用标准的UTC/GMT时间(后面统称UTC),或者保留时区的字符串(如:2017-02-24T23:57:06.000+0000)。

  1. Android中表示日期时间的类型,有Date、Calendar,均为系统默认时区的时间,都是与时区相关的。
  2. SimpleDateFormat对象本身也是跟时区相关。
  3. Calendar在手动修改时区后,不能使用calendar.getTime方法来直接获取Date日期,因为此时的日期与setTime时的值相同,想要正确获取修改时区后的时间,应该通过Calendar的get方法
  4. TimeZone,我们可以通过TimeZone对象获取关于系统默认时区及其相关的详细信息。
    // Date、Calendar SimpleDateFormat 默认时区
    Date date = new Date();
    System.out.println(date);

    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getTime());

    SimpleDateFormat sdf = new SimpleDateFormat("E MMM dd hh:mm:ss z yyy", Locale.US);
    System.out.println(sdf.format(date));

    calendar.setTimeZone(TimeZone.getTimeZone("America/New_York"));
    SimpleDateFormat calendarSdf = new SimpleDateFormat("M.dd", Locale.US);
    System.out.println(calendarSdf.format(calendar.getTime()));
    // 修改时区之后,通过calendar的get方法
    System.out.println((calendar.get(Calendar.MONTH) + 1) + "." + calendar.get(Calendar.DAY_OF_MONTH));

    // 输出
    Mon Feb 27 12:40:17 CST 2017
    Mon Feb 27 12:40:17 CST 2017
    2.27
    2.26
    Mon Feb 27 12:40:17 CST 2017

读取本地时区信息

    /**
     * Gets the default <code>TimeZone</code> for this host.
     * The source of the default <code>TimeZone</code>
     * may vary with implementation.
     * @return a default <code>TimeZone</code>.
     * @see #setDefault
     */
    public static TimeZone getDefault() {
        return (TimeZone) getDefaultRef().clone();
    }


    TimeZone timeZone = TimeZone.getDefault()
    String id = timeZone.getID(); //获取时区id
    String name = timeZone.getDisplayName(); //获取名字
    int rawOffset = timeZone.getRawOffset(); //获取时差,返回值毫秒
    System.out.println("TimeZone ID = " + id);
    System.out.println("TimeZone Name = " + name);
    System.out.println("TimeZone RawOffset = " + rawOffset + "ms");
    rawOffset /= 1000;// 转换成秒
    System.out.println("TimeZone RawOffset = " + rawOffset / 3600 + "h"
            + (rawOffset % 3600) / 60 + "m"
            + (rawOffset % 3600) % 60 + "s");

    // 输出
    TimeZone ID = Asia/Shanghai
    TimeZone Name = 中国标准时间
    TimeZone RawOffset = 28800000ms
    TimeZone RawOffset = 8h0m0s

日期时间格式(官方定义)

字符意义类型例子
GEra designatorTextAD
yYearYear1996; 96
MMonth in yearMonthJuly; Jul; 07
wWeek in yearNumber27
WWeek in monthNumber2
DDay in yearNumber189
dDay in monthNumber10
FDay of week in monthNumber2
EDay in weekTextTuesday; Tue
aAm/pm markerTextPM
HHour in day (0-23)Number0
kHour in day (1-24)Number24
KHour in am/pm (0-11)Number0
hHour in am/pm (1-12)Number12
mMinute in hourNumber30
sSecond in minuteNumber55
SMillisecondNumber978
zTime zoneGeneral time zoneCST; GMT-08:00
ZTime zoneRFC 822 time zone +0800

    Date date = new Date();

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss z", Locale.US);
    // 默认 是本地时区
    System.out.println(sdf.format(date));
    // GMT 时间
    sdf.setTimeZone(TimeZone.getTimeZone("GMT+9"));
    System.out.println(sdf.format(date));

    // 美国东部标准时间
    sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
    System.out.println(sdf.format(date));

    // 中国标准时间
    sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
    System.out.println(sdf.format(date));

    // 输出
    2017-02-26T16:56:14 CST
    2017-02-26T17:56:14 GMT+09:00
    2017-02-26T03:56:14 EST
    2017-02-26T16:56:14 CST

z VS Z

小z(General time zone):如果有对应的时区名称,显示名称,否则以GMTOffsetTimeZone格式显示。

// 时区名称
2017-02-27T13:37:40 CST
// GMTOffsetTimeZone
GMT Sign Hours : Minutes
2017-02-27T14:37:40 GMT+09:00

大Z:RFC 822 4-digit 格式

// RFC822TimeZone:Sign TwoDigitHours Minutes
2017-02-27T13:37:40 +0800

不能使用 时区名称来获取 时区

因为CST可以同时表示美国,澳大利亚,中国,古巴四个国家的标准时间:

Central Standard Time (USA) UT-6:00
Central Standard Time (Australia) UT+9:30
China Standard Time UT+8:00
Cuba Standard Time UT-4:00

    // 不能使用 时区名称来获取 时区,发现EST是准的,但是CST不准。

    Date date = new Date();

    SimpleDateFormat sdfZ = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss Z", Locale.US);
    // 默认 是本地时区
    System.out.println(sdfZ.format(date));
    sdfZ.setTimeZone(TimeZone.getTimeZone("CST"));
    System.out.println(sdfZ.format(date));

    // 美国东部标准时间
    sdfZ.setTimeZone(TimeZone.getTimeZone("EST"));
    System.out.println(sdfZ.format(date));
    sdfZ.setTimeZone(TimeZone.getTimeZone("America/New_York"));
    System.out.println(sdfZ.format(date));
    // 输出
    2017-02-27T13:52:44 +0800
    2017-02-26T23:52:44 -0600
    2017-02-27T00:52:44 -0500
    2017-02-27T00:52:44 -0500

其他

1. Date formats(SimpleDateFormat)不是线程安全的,推荐一个线程一个实例。

破解updating the version of com.google.android.gms to 9.0.0.

使用Firebase遇到的问题:

Error:Execution failed for task ':stocks:processDebugGoogleServices'.

Please fix the version conflict either by updating the version of the google-services plugin 

(information about the latest version is available at https://bintray.com/android/android-tools/com.google.gms.google-services/) 

or updating the version of com.google.android.gms to 9.0.0.

只需要把”apply plugin”放到build.gradle文件的最后…

apply plugin: 'com.android.application'

android {
// ...
}

dependencies {
// ...
compile 'com.google.firebase:firebase-core:10.0.1'

// Getting a "Could not find" error? Make sure you have
// the latest Google Repository in the Android SDK manager
}

// ADD THIS AT THE BOTTOM (最后面...)
apply plugin: 'com.google.gms.google-services'

哎…***************(略)…here

DataBinding的BR生成规则

生成的BR.class

package com.github.captain_miao.uniqueadapter.library;

public class BR {
    public static int _all = 0;
    public static int presenter = 1;
    public static int viewModel = 2;

    public BR() {
    }
}

从生成的规则上看,是通过变量名(viewModel/presenter)来生成ID的,所以相同的变量名共用相同的ID,所以使用相同的变量,可以减少ID哈。

也让UniqueAdapter成为可能,可以使用相同的ViewHolder。


@Override
public void onBindViewHolder(UniqueViewHolder holder, int position) {
    ItemModel item = getItem(position);
    holder.dataBinding.setVariable(com.github.captain_miao.uniqueadapter.library.BR.viewModel, item);
    if (mPresenter != null) {
        holder.dataBinding.setVariable(com.github.captain_miao.uniqueadapter.library.BR.presenter, mPresenter);
    }
    holder.dataBinding.executePendingBindings();
}

借助DataBinding,只需要一个Adapter。

背景

App有很多的List界面,每个界面都有不同的数据,然后都需要写个Adapter。当使用DataBinding之后,还是要写Adapter,但是需要写的内容越来越少。

/**
 * @author YanLu
 * @since 16/7/12
 */
public class DealerVehicleAdapter 
        extends BaseWrapperRecyclerAdapter<Vehicle, RecyclerView.ViewHolder> {
    private DealerVehiclePresenter mPresenter;

    public DealerVehicleAdapter(DealerVehiclePresenter presenter) {
        this.mPresenter = presenter;
    }


    @Override
    public RecyclerView.ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.rv_item_view_dealer_vehicle, parent, false);

        return new DealerVehicleAdapter.ViewHolder(view);
    }

    @Override
    public void onBindItemViewHolder(RecyclerView.ViewHolder holder, int position) {
        final Vehicle vehicle = getItem(position);
        if(holder instanceof ViewHolder){
            ViewHolder viewHolder =(ViewHolder) holder;
            viewHolder.binding.setVariable(BR.viewModel, vehicle);
            viewHolder.binding.setVariable(BR.presenter, mPresenter);
            viewHolder.binding.executePendingBindings();
        }
    }


    public static class ViewHolder extends RecyclerView.ViewHolder {
        private ViewDataBinding binding;

        public ViewHolder(View itemView) {
            super(itemView);
            binding = DataBindingUtil.bind(itemView);
        }

        public ViewDataBinding getBinding() {
            return binding;
        }

    }

}

这个Adapter中,只做了两个事情:

1. ViewDataBinding创建

只需要一个参数:R.layout.rv_item_view

    @Override
    public RecyclerView.ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.rv_item_view_dealer_vehicle, parent, false);

        return new DealerVehicleAdapter.ViewHolder(view);
    }
2. DataBingding变量赋值

只需要二个参数:viewModel 和 presenter

    @Override
    public void onBindItemViewHolder(RecyclerView.ViewHolder holder, int position) {
        final Vehicle vehicle = getItem(position);
        if(holder instanceof ViewHolder){
            ViewHolder viewHolder =(ViewHolder) holder;
            viewHolder.binding.setVariable(BR.viewModel, vehicle);
            viewHolder.binding.setVariable(BR.presenter, mPresenter);
            viewHolder.binding.executePendingBindings();
        }
    }
那能不能使用方提供layoutId、viewModel、presenter,就只需要一个Adapter呢?

实现

为了ItemModel不至于太多内容,presenter由Adapter自己提供,其他由ItemModel提供。

ItemModel
public interface ItemModel {
    // itemView layout
    int getItemViewLayoutId();
}
UniqueAdapter

由于要支持多种ItemViewType,直接使用ItemModel.getItemViewLayoutId(),全局唯一的。
如果要支持头部、尾部,可以使用负数来区分。

/**
 * @author YanLu
 * @since 16/7/12
 */
public class DealerVehicleAdapter 
        extends BaseWrapperRecyclerAdapter<Vehicle, RecyclerView.ViewHolder> {
    private DealerVehiclePresenter mPresenter;
 
    public DealerVehicleAdapter(DealerVehiclePresenter presenter) {
        this.mPresenter = presenter;
    }
 
 
    @Override
    public RecyclerView.ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.rv_item_view_dealer_vehicle, parent, false);
 
        return new DealerVehicleAdapter.ViewHolder(view);
    }
 
    @Override
    public void onBindItemViewHolder(RecyclerView.ViewHolder holder, int position) {
        final Vehicle vehicle = getItem(position);
        if(holder instanceof ViewHolder){
            ViewHolder viewHolder =(ViewHolder) holder;
            viewHolder.getBinding().setVariable(BR.viewModel, vehicle);
            viewHolder.getBinding().setVariable(BR.presenter, mPresenter);
            viewHolder.getBinding().executePendingBindings();
        }
    }
 
 
    public static class ViewHolder extends RecyclerView.ViewHolder {
        private ViewDataBinding binding;
 
        public ViewHolder(View itemView) {
            super(itemView);
            binding = DataBindingUtil.bind(itemView);
        }
 
        public ViewDataBinding getBinding() {
            return binding;
        }
 
    }
 
}

兼容 旧的Adapter?

1. ItemModel很容易兼容,只需要implements ItemModel。
2. 旧的Adapter 继承 BaseUniqueAdapter

protected final List<? extends ItemModel> mDataList;


public UniqueAdapter(@NonNull List<? extends ItemModel> dataList) {
    this.mDataList = dataList;
}

public UniqueAdapter(@NonNull List<? extends ItemModel> dataList, UniquePresenter<? extends ItemModel> presenter) {
    this.mDataList = dataList;
    this.mPresenter = presenter;
}

public ItemModel getItem(int position) {
    return mDataList.get(position);
}

@Override
public int getItemCount() {
    return mDataList.size();
}

点击事件处理

android:onClick=”@{(v) -> presenter.onClick(v, viewModel)}”
这个至少可以满足80%的点击事件,不能满足的使用方自己定义哈。

public interface UniquePresenter<T extends ItemModel> extends Serializable {
    void onClick(View view, T t);
}


    // layout
    <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="@{(v) -> presenter.onClick(v, viewModel)}"
        >

GitHub地址

UniqueAdapter

Picasso Callback/Target没有回调(特别是第一次)

Picasso的Targets/Callback是弱引用,可能被GC回收,也可能没回收,所以回调变成了随机事件。

  DeferredRequestCreator(RequestCreator creator, ImageView target, Callback callback) {
    this.creator = creator;
    // 定义弱引用
    this.target = new WeakReference<ImageView>(target);
    this.callback = callback;

    ...
  }

  @Override public boolean onPreDraw() {
    ImageView target = this.target.get();
    if (target == null) {
      // 被回收 直接返回了
      return true;
    }
    ...
  }

解决办法,将Targets/Callback强引用,用完之后释放。来自stackoverflow

// 直接下载图片,需要在work thread
Picasso.with(context).load(url).get();

// Callback
ImageView profile = new ImageView(context);
Picasso.with(context).load(URL).into(profile, new Callback() {
    @Override
    public void onSuccess() {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {//You will get your bitmap here

                Bitmap innerBitmap = ((BitmapDrawable) profile.getDrawable()).getBitmap();
            }
        }, 100);
    }

    @Override
    public void onError() {

    }
});

//or

// Target
final List<Target> targets = new ArrayList<Target>();
for (int i = 0; i < 3; i++) { 
    final int k=i;
    Target target = new Target() {

        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {                         
            Log.i("load", "Ok " + k);   
            //use bitmap for add marker to map
            targets.remove(this);                
        }

        @Override
        public void onBitmapFailed(Drawable errorDrawable) {
            targets.remove(this);
        }

        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {
            Log.i("load", "first " + k);
        }
    }
    targets.add(target);
    Picasso.with(this)
        .load(ListA.get(i).getImage()) //image
        .resize(100, 100)
        .transform(new ImageTrans_CircleTransform())
        .into(target);  
}

Animate all the things. Transitions in Android(译)

原文链接

Animate all the things. Transitions in Android

Github

Transitions-Everywhere

译文开始

嘿,各位Android开发者。我将告诉你们一些Android动画的新动向。Google最终发布了Material Design: 动画不仅仅是iOS了。其中的一个新概念: Material motion.

“动作使得(界面交互)变得有意义。即便对象在不断的变化,用户依然可以连续直观地体验到其过程。在 material design 的世界中,Motion使得空间关系,功能性和意向的表达 变得连贯而更具美感。”
– Material Design guidelines.

实际上,创建动画是个耗时的过程。在过往的开发中,只需要调用setVisibility(View.VISIBLE)就显示新功能的业务逻辑,肯定是充满诱惑力的。但是记住:每次你忽略补充有意义的过渡动画的机会,在世界某个地方就多了一名悲伤的设计师。
如果我告诉你,动画要比你想象的省力?你有没有听过Transitions API?是的,这是谷歌推广的Activities之间精美的动画,可悲的是,它仅适用于Android5.0以后。所以实际上没有人使用它。但是想象一下,这个API可以在许多不同的情况下得到有效利用,并且更令人兴奋的是,可在旧Android版本中使用。

让我们从一些历史开始

Android 4.0中,介绍了ViewGroup新的animateLayoutChange参数。但是,就算调用getLayoutTransition()和配置里面的一些东西,仍然不稳定,不够灵活。所以你不能使用它做太多事情。
4.4 Kitkat带给我们场景转换的想法。场景在技术上是指场景根中(布局容器)所有View的状态。转场(Transition)是将View的动画集合,从一个场景平滑过渡到另一个场景。举几个栗子,它会更加吸引人?当然。

想象一下,我们有一个按钮

点击之后,我们想在按钮的下面出现一串文字。这是布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/transitions_container" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical">
 
    <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="DO MAGIC"/>
 
    <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="Transitions are awesome!" android:visibility="gone"/>
 
</LinearLayout>

然后代码中添加一个click listener:

final ViewGroup transitionsContainer = (ViewGroup) view.findViewById(R.id.transitions_container);
final TextView text = (TextView) transitionsContainer.findViewById(R.id.text);
final Button button = (Button) transitionsContainer.findViewById(R.id.button);
 
button.setOnClickListener(new View.OnClickListener() {

    boolean visible;

    @Override
    public void onClick(View v) {
        TransitionManager.beginDelayedTransition(transitionsContainer);
        visible = !visible;
        text.setVisibility(visible ? View.VISIBLE : View.GONE);          
    }

});

效果:
have_a_text_appeared

还不错。一行代码搞定了动画。有趣的事情不只是text显示有动画,按钮的位置也有动画。当TextView显示是引起的布局变化,Transition框架会自动完成布局变化的动画,所以你不需要自己来做。就算前一个动画还在运行,你也可以启动一个新的动画。Transition框架会停止正在运行的动画,然后在当前位置继续动画。所有的都是自动的。

通过beginDelayedTransition(final ViewGroup sceneRoot, Transition transition)第二个参数,我们可以制定动画类型。

Transition的简单类型

  • ChangeBounds. 用于View的位置和大小动画。上面的例子就是移动按钮。
  • Fade. 它继承Visibility,并执行最流行的动画 – 淡入和淡出。上面的例子就是TextView的显示(淡入和淡出)。
  • TransitionSet. 它是Transition组合成的动画组。他们可以一起或顺序启动,可以通过setOrdering()来修改顺序。
  • AutoTransition. 它其实是一个TransitionSet,包含顺序动画:Fade out, ChangeBounds and Fade in。首先,View淡出动画,然后View的位置和大小变化动画,最后View淡入动画。

Backport(向后移植)

每个人都希望只写一个实现,在每一个Android版本中有一致的行为。希望我们使用Transitions API上,能够做到这一点。前一段时间我已经发现了两个非常相似的库::

github.com/guerwan/TransitionsBackport
github.com/pardom/TransitionSupportLibrary

他们不再维护,他们都错过了一些东西,还可以向后移植。所以基于他们两个库,我创建了一个自己的库,并增加了很多与老Android版本兼容的东西。从Lollipop到Marshmallow所有API的变化都已经合并了。
所以,我们有它:Transitions Everywhere 使Transitions API支持Android 4.0及以上。
开始使用它,只需要添加依赖(gradle):

dependencies {
    compile "com.andkulikov:transitionseverywhere:1.6.5"
}

然后将所有import android.transition.*,替换成com.transitionseverywhere.*

transitions_everywhere

我们还可以做什么

首先, 我们可以修改时长(duration)、插值器(interpolator)和延时启动(start delay):

transition.setDuration(300);
transition.setInterpolator(new FastOutSlowInInterpolator());
transition.setStartDelay(200);

让我们看看其他的可用的Transition类型

Slide(滑动)

就像 Fade transition, extends Visibility class. 它帮助View从一测滑向另一测,例子 Slide(Gravity.RIGHT):
slide_from_right

Explode and Propagation

Explode 很像Slide, 只是view在计算的方向上滑动,由Transition epicenter提供 (通过setEpicenterCallback方法来设置).
TransitionPropagation 为每个动画计算启动延时。比如, Explode 默认使用 CircularPropagation。动画启动延时取决于view和epicenter之间的距离。要使用它, 调用setPropagation

比方说,我们有RecyclerView与GridLayoutManager,在点击任何一个元素时,我们删除所有元素。如下所示:

public void onClick(View clickedView) {
    // save rect of view in screen coordinates
    final Rect viewRect = new Rect();
    clickedView.getGlobalVisibleRect(viewRect);
 
    // create Explode transition with epicenter
    Transition explode = new Explode()
        .setEpicenterCallback(new Transition.EpicenterCallback() {
            @Override
            public Rect onGetEpicenter(Transition transition) {
                return viewRect;
            }
        });
    explode.setDuration(1000);
    TransitionManager.beginDelayedTransition(recyclerView, explode);
 
    // remove all views from Recycler View
    recyclerView.setAdapter(null);
}

explode_and_propagation

ChangeImageTransform

ChangeImageTransform 图像矩阵变化动画. 当我们改变ImageView的scaleType时,它是非常有用。大多数情况下,都和ChangeBounds配合使用:位置、大小、scaleType的变化动画。

TransitionManager.beginDelayedTransition(transitionsContainer, new TransitionSet()
    .addTransition(new ChangeBounds())
    .addTransition(new ChangeImageTransform()));
 
ViewGroup.LayoutParams params = imageView.getLayoutParams();
params.height = expanded ? ViewGroup.LayoutParams.MATCH_PARENT : 
    ViewGroup.LayoutParams.WRAP_CONTENT;
imageView.setLayoutParams(params);
 
imageView.setScaleType(expanded ? ImageView.ScaleType.CENTER_CROP : 
    ImageView.ScaleType.FIT_CENTER);

change_image_transform

Path (Curved) motion

“真实世界的力量,例如重力,驱动一个元素沿弧线的移动而非沿直线。”
– Material Design guidelines.

对于二维坐标的View转移操作(适用于使用ChangeBounds位置变化的例子),我们可以使用ChangeBounds.setPathMotion()方法设置移动的路径.

TransitionManager.beginDelayedTransition(transitionsContainer,
    new ChangeBounds().setPathMotion(new ArcMotion()).setDuration(500));
 
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) button.getLayoutParams();
params.gravity = isReturnAnimation ? (Gravity.LEFT | Gravity.TOP) :
    (Gravity.BOTTOM | Gravity.RIGHT);
button.setLayoutParams(params);

path_curved_motion

TransitionName

我们需要删除容器的所有view,并添加一组新的view。而一些新的元素实际上和之前创建的是相同的。我们如何告诉框架,删除哪些元素,哪些元素移动到一个新的位置? 简单。只需调用静态方法TransitionManager.setTransitionName(View v, String transitionName),为每个view提供唯一的名称。
例如,如果我们创建标题列表,每次点击按钮重新创建View并打乱排序:

createViews(inflater, layout, titles);
shuffleButton.setOnClickListener(new View.OnClickListener() {
 
    @Override
    public void onClick(View v) {
        TransitionManager.beginDelayedTransition(layout, new ChangeBounds());
        Collections.shuffle(titles);
        createViews(inflater, layout, titles);
    }
 
});
 
// 需要为每个view设置transition name

private static void createViews(LayoutInflater inflater, ViewGroup layout, List&lt;String&gt; titles) {
    layout.removeAllViews();
    for (String title : titles) {
        TextView textView = (TextView) inflater.inflate(R.layout.text_view, layout, false);
        textView.setText(title);
        TransitionManager.setTransitionName(textView, title);
        layout.addView(textView);
    }
}

transition_name

Scale

这个不是Transitions API的中的,而是我增加的。它可以使用缩放动画来显示或隐藏view。简单的使用:new Scale():
scale

也可以和其他Transition组合使用 例如, Fade:

TransitionSet set = new TransitionSet()
    .addTransition(new Scale(0.7f))
    .addTransition(new Fade())
    .setInterpolator(visible ? new LinearOutSlowInInterpolator() : 
        new FastOutLinearInInterpolator());

TransitionManager.beginDelayedTransition(transitionsContainer, set);
text2.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);

scale_fade

Recolor

猜一下这一个是什么动画。 棒棒哒!背景和/或文本颜色的变化动画。

TransitionManager.beginDelayedTransition(transitionsContainer, new Recolor());
 
button.setTextColor(getResources().getColor(!isColorsInverted ? R.color.second_accent :R.color.accent));
button.setBackgroundDrawable(
    new ColorDrawable(getResources().getColor(!mColorsInverted ? R.color.accent :
        R.color.second_accent)));

re_color

Rotate

没时间解释, 例如:

TransitionManager.beginDelayedTransition(transitionsContainer, new Rotate());
icon.setRotation(isRotated ? 135 : 0);

rotate

ChangeText

帮助为文字变化增加淡入淡出动画,例如:

TransitionManager.beginDelayedTransition(transitionsContainer,
    new ChangeText().setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN));
 textView.setText(isSecondText ? TEXT_2 : TEXT_1);

change_text

Targets

Transitions很容易配置. 你可以为任何Transition指定目标view,而且只有它们的可以动画.

Methods to add target:

  • addTarget(View target) — view itself
  • addTarget(int targetViewId) — id of view
  • addTarget(String targetName) — do you remember about method TransitionManager.setTransitionName?
  • addTarget(Class targetType) — for example android.widget.TextView.class

To remove target:

  • removeTarget(View target)
  • removeTarget(int targetId)
  • removeTarget(String targetName)
  • removeTarget(Class target)

To exclude some views:

  • excludeTarget(View target, boolean exclude)
  • excludeTarget(int targetId, boolean exclude)
  • excludeTarget(Class type, boolean exclude)
  • excludeTarget(Class type, boolean exclude)

And for excluding all children of some ViewGroup:

  • excludeChildren(View target, boolean exclude)
  • excludeChildren(int targetId, boolean exclude)
  • excludeChildren(Class type, boolean exclude)

Create Transition with xml

Transition可以xml. Xml创建, 文件目录 res/anim。例如:

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:app="http://schemas.android.com/apk/res-auto"
              app:transitionOrdering="together"
              app:duration="400">
    <changeBounds/>
    <changeImageTransform/>
    <fade
       app:fadingMode="fade_in"
       app:startDelay="200">
        <targets>
            <target app:targetId="@id/transition_title"/>
        </targets>
    </fade>
</transitionSet>

// And inflating:
TransitionInflater.from(getContext()).inflateTransition(R.anim.my_the_best_transition);

Activity and Fragment transitions

Activity Transitions没办法向后移植,对不起(sadpanda)。大量的逻辑隐藏在Activity中。这同样适用于Fragment transitions。我们要创造我们自己的Fragment transitions逻辑。

Custom Transitions

对于任何view,Transitions可以用于各种用途。让我们创建一些独特-我们自己的Transition。所有的这些我们都需要实现三个方法: captureStartValues,captureEndValues and createAnimator. 前两个方法是动画之前和动画之后捕获view的状态。

我们将创建一个水平进度条(ProgressBar)进度平滑变化的动画:

private class ProgressTransition extends Transition {
 
    /**
     * Property is like a helper that contain setter and getter in one place
     */
    private static final Property&lt;ProgressBar, Integer&gt; PROGRESS_PROPERTY = 
        new IntProperty&lt;ProgressBar&gt;() {
 
        @Override
        public void setValue(ProgressBar progressBar, int value) {
            progressBar.setProgress(value);
        }
 
        @Override
        public Integer get(ProgressBar progressBar) {
            return progressBar.getProgress();
        }
    };
 
    /**
      * Internal name of property. Like a intent bundles 
      */
    private static final String PROPNAME_PROGRESS = "ProgressTransition:progress";
 
    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }
 
    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }
 
    private void captureValues(TransitionValues transitionValues) {
        if (transitionValues.view instanceof ProgressBar) {
            // save current progress in the values map
            ProgressBar progressBar = ((ProgressBar) transitionValues.view);
            transitionValues.values.put(PROPNAME_PROGRESS, progressBar.getProgress());
        }
    }
 
    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, 
            TransitionValues endValues) {
        if (startValues != null &amp;&amp; endValues != null &amp;&amp; endValues.view instanceof ProgressBar) {
            ProgressBar progressBar = (ProgressBar) endValues.view;
            int start = (Integer) startValues.values.get(PROPNAME_PROGRESS);
            int end = (Integer) endValues.values.get(PROPNAME_PROGRESS);
            if (start != end) {
                // first of all we need to apply the start value, because right now
                // the view is have end value
                progressBar.setProgress(start);
                // create animator with our progressBar, property and end value
                return ObjectAnimator.ofInt(progressBar, PROGRESS_PROPERTY, end);
            }
         }
         return null;
    }
}

然后怎么使用我们全新的 Transition?

private void setProgress(int value) {
    TransitionManager.beginDelayedTransition(mTransitionsContainer, new ProgressTransition());
    value = Math.max(0, Math.min(100, value));
    mProgressBar.setProgress(value);
}

效果:
progress_transition

PS

本文中所有的例子,都在Github的工程中:

github.com/andkulikov/transitions-everywhere

Never stop moving (and transitioning).

如何安装CocoaPods

安装ruby

CocoaPods使用ruby实现的,所以尽量使用配套ruby版本。
ps:不要用 RVM/rbenv 来安装,脚本错误繁多,而且下载源都很慢。

brew install ruby

设置镜像地址

你懂的,非常慢。

$ gem sources -a http://ruby.taobao.org/

$ gem sources --remove https://rubygems.org/

$ gem sources --remove http://rubygems.org/

# 查看 源列表
$ gem sources -l

安装CocoaPods

$ [sudo] gem install cocoapods

下载spec-repo

Master spec-repo 文件太大,做些处理,from here: Master-Spec-Repo-Rate-Limiting-Post-Mortem

$ cd ~/.cocoapods/repos
$ git clone https://github.com/CocoaPods/Specs.git master

# long long time...


使用CocoaPods

在工程目录执行下面的命令,这里还是慢。。。不知道还有什么办法

$ pod install --verbose --no-repo-update
$ pod update --verbose --no-repo-update

Xcode 打开 .xcworkspace,不是.xcodeproj

RadioGroup的一个bug

如果设置了默认选择,也没有给RadioButton添加ID,那么将没办法取消选择了。

<RadioGroup android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal">

    <RadioButton android:layout_width="wrap_content" android:layout_height="wrap_content"/>

    <RadioButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="true"/>

</RadioGroup>

这个bug的原因:

RadioGroup在设置选择项的时候,RadioButton没有自动生成ID,导致选择项的ID为-1,等同于没选择。

创建流程:

  1. RadioButton创建
  2. RadioButton设置选中状态
  3. RadioGroup设置选中项(setCheckedId)
  4. RadioButton创建完成,添加到RadioGroup中

RadioButton创建的时候,回调onCheckedChanged()

private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        // prevents from infinite recursion
        if (mProtectFromCheckedChange) {
            return;
        }

        mProtectFromCheckedChange = true;
        if (mCheckedId != -1) {
            setCheckedStateForView(mCheckedId, false);
        }
        mProtectFromCheckedChange = false;

        int id = buttonView.getId();//这里是-1
        setCheckedId(id);
    }
}

RadioButton创建完成,添加到RadioGroup中

@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
    if (child instanceof RadioButton) {
        final RadioButton button = (RadioButton) child;
        if (button.isChecked()) {
            mProtectFromCheckedChange = true;
            if (mCheckedId != -1) {
                setCheckedStateForView(mCheckedId, false);
            }
            mProtectFromCheckedChange = false;
            setCheckedId(button.getId());
        }
    }

    super.addView(child, index, params);
}

// 这里才生成ID (在add/removeView的时候回调)
public void onChildViewAdded(View parent, View child) {
    if (parent == RadioGroup.this && child instanceof RadioButton) {
        int id = child.getId();
        // generates an id if it's missing
        if (id == View.NO_ID) {
            id = View.generateViewId();
            child.setId(id);
        }
        ((RadioButton) child).setOnCheckedChangeWidgetListener(
                mChildOnCheckedChangeListener);
    }

    if (mOnHierarchyChangeListener != null) {
        mOnHierarchyChangeListener.onChildViewAdded(parent, child);
    }
}

最简单的解决办法:

给RadioButton加个ID

如果是动态添加RadioButton:参考这里(stackoverflow.com)

App开发如何保持迭代速度

前提

  1. 固定人员投入
  2. 固定上班时间
  3. 产品功能确定,不考虑功能是否有前景

以上条件相同的情况下,如何保持迭代的速度(更快的发布给用户使用、更快的反馈改进)

App开发的特点

App开发,涉及多个角色的协作,客户端依赖所有端:产品端、服务端、设计端、测试端。App的一个业务界面,可以做1天,也可以做1周(5天)。完全取决于产品设计、服务端接口、交互和视觉。

以一个按钮(button)为例

社交类产品,有新好友通知列表,可以:添加、同意、邀请好友三种操作。
最简的方式:默认button
经过设计的:每个操作,都不同的颜色来区分。

来看看这两种方式工作量的差别:

按钮文字(1)文字颜色(2)按钮背景(2)宽(1)高(1)总计
默认Add/Accept/Invite默认默认默认默认1
AddAdd22默认15
AcceptAccept22默认15
InviteInvite22默认15

吓尿了,5*3/1 = 15 倍,效果也确实”好看”。
p_i_t

产品设计

  1. 约定迭代周期(比如:2周或者1个月)
  2. 对于要实现的功能,在迭代周期内进行拆分(这个能力非常重要:优先级和紧急程度,再重要的功能也是要有个排序)
  3. 最简产品规划,但为未来做预留,但不为未来而实现(比如:推送消息,为了避免骚扰用户,进行发送频率限制?)
  4. 迭代内,不要变更需求了

服务端接口

  1. 优先定义好接口,接口就是客户端的血液。
  2. 接口的稳定,接口的改变导致Android、iOS都需要修改,两倍工作量额。
  3. 接口的异常情况说明
  4. 为未来做架构,根据需要实现
  5. 开发环境与测试环境分离

交互和视觉

  1. 风格统一,不要随性设计
  2. 多借鉴,少抄袭
  3. 界面的交互和视觉,紧密结合产品特点(否则一个界面展示的内容,得调用好几个API,扯着蛋了)

客户端

  1. 多封装代码,方便使用(app本质就是数据、UI、每个界面共同的东西比较多)
  2. 对于View慢慢封装不变的东西,把变的东西做薄。
  3. 除了View控件,对于View的界面慎重复用