英文原文:From design to android, part 1
多亏了 Dribbble 和 MaterialUp这样的设计平台,我们这些开发者才有机会接触到大量的概念设计资源。尽管如此,有时候有些细节几乎是不可能实现的,部分用户体验并没有被考虑。
鉴于此,我觉得建立一个这样的项目会比较有意思:选择一些Dribbble 或者 MaterialUp上的设计资源,在安卓上实现它们,然后撰写一些列的文章来讲解实现的细节以及我认为比较重要的安卓界面实现技巧。
概念设计
这是我为第一部分选择的概念设计,简单但是足以涵盖一些有趣的话题了,比如 ConstraintLayout & chains, DataBinding,UI层次结构性能的重要性以及scenes(场景)。
-
该概念设计的作者是 Johnyvino,发布在 MaterialUp上。
-
其实现可以在GitHub的这个仓库上找到。
让我们开始吧!
支持库中的Bottom sheets
在演示图中,bottom sheet被用来引导用户完成购买过程。在安卓中你可以找到几个第三方库来实现这种视图,比如Umano的 AndroidSlidingUpPanel library。
其实最近 bottoms sheets被引入了 design support library,因此我决定在这里使用它,学习该如何使用。
Bottom sheets有两种使用方式,一种是Bottom sheets作为main view 的一部分(在 CoordinatorLayout中的一个viewgroup上使用BottomSheetBehavior),另一种是模态对话框似的Bottom sheets,使用BottomSheetDialogFragment来实现。
对于我们的例子我选择BottomSheetDialogFragment ,因为这里的信息是以模态的形式显示的。这种Bottom sheets实现的代码和DialogFragment的用法类似。
HomeActivity.java
OrderDialogFragment.newInstance( fakeProducts.get(position)) .show(getSupportFragmentManager(),null);
OrderDialogFragment.java
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); binding = FragmentOrderFormBinding.inflate( inflater, container, false); return binding.getRoot(); }
ConstraintLayout
Google I/O '16上发布的这个强大的布局控件最近迎来了它的稳定版本(1.0.2)。
这个控件的最大好处在于,它可以让你以最扁平的层次,构造复杂的,自适应的布局。
通常建议安卓的布局中要避免使用层次很深的视图结构,因为这样会降低性能,而且会增加UI绘制到屏幕的时间。而如果使用ConstraintLayout,这样的要求自动就满足了。
基本地,ConstraintLayout的工作原理类似RelativeLayout,即定义视图与屏幕之间的关系。但是ConstraintLayout除了性能更好之外,在Android Studio的图形编辑器中的表现也真的很好。
除此之外还有更多有趣的机制比如view之间的chain(不同的view相互约束),或者guidelines。
chain的使用
假设我们有两个view,A和B,分别被约束在屏幕的右边沿和左边沿。如果A设定在B的左边,而B设定在A的右边,这样这组view就会被特殊处理,在约束布局中这样的情况被称之为chain。
使用Android Studio的布局编辑器的上下文菜单很容易就可以创建一个chain。默认创建的是spread chain,即视图均匀分布。
Chain 类型
在这个例子中,我们只使用spread 和 packed chain,但是你可以根据自己的需要选择,最近谷歌对ConstraintLayout的文档增加了对chain的说明,没有理由不用。
被选中view的过渡动画
这看起来比较简单,每当用户点击一个产品参数的时候,被选择的view就过渡到左下角的标签旁边。
为此我们在被点击的的view的parent中添加一个新的view。
OrderDialogFragment.java
private void transitionSelectedView(View v) { final View selectionView = createSelectionView(v); binding.mainContainer.addView(selectionView); startCloneAnimation(selectionView, getTargetView(v)); }
然后使用TransitionManager的beginDelayedTransition来做过渡动画。这个方法将检测布局是否有变化,如果有,使用传入的transition来执行动画。
OrderDialogFragment.java
private void startCloneAnimation(View clonedView, View targetView) { clonedView.post(() -> { TransitionManager.beginDelayedTransition( (ViewGroup) binding.getRoot(), selectedViewTransition); // 触发transition clonedView.setLayoutParams(SelectedParamsFactory .endParams(clonedView, targetView)); }); }
ViewSwitcher
ViewSwitcher,Android SDK一个并不十分流行的控件,它可以用进入和退出动画来切换两个view。这和我们购买过程中在代表两个步骤的布局之间切换是完全符合的。
fragment_order_form.xml
<ViewSwitcher android:id="@+id/switcher" android:layout_width="match_parent" android:layout_height="wrap_content" android:inAnimation="@anim/slide_in_right" android:outAnimation="@anim/slide_out_left" > <include android:id="@+id/layout_step1" layout="@layout/layout_form_order_step1" /> <include android:id="@+id/layout_step2" layout="@layout/layout_form_order_step2" /> </ViewSwitcher>
OrderDialogFragment.java
private void showDeliveryForm() { binding.switcher.setDisplayedChild(1); initOrderStepTwoView(binding.layoutStep2); }
Databinding(数据绑定)
这个设计的实现我使用了Databinding,对我来说在一个对象中包含所有的布局结构是可以接受的,但是Databading还有其它一些机制值得一提。
节省click listener的书写次数
因为这些布局有大量的listener需要处理,我决定为用Databinding绑定一个listener对象来处理onClick。在xml中,为每个view的onClick属性设置方法名,然后事件就会定向到listener对象的相应方法中。
layout_form_order_step1.xml
<data> <variable name="listener" type="com.saulmm.cui.OrderDialogFragment.Step1Listener" /> </data> <CircleImageView android:id="@+id/img_color_blue" style="@style/Widget.Color" android:onClick="@{listener::onColorSelected}" /> <CircleImageView android:id="@+id/img_color_blue" style="@style/Widget.Color" android:onClick="@{listener::onColorSelected}" />
OrderDialogFragment.java
layoutStep1Binding.setListener(new Step1Listener() { @Override public void onSizeSelected(View v) { // ... } @Override public void onColorSelected(View v) { // ... } })
使用Spannables + databinding + ConstraintLayout来减少view
让我们花点时间来思考如何实现下面这部分设计
一种办法是每个item设置一个垂直的LinearLayout,然后3个item被包含在一个水平的LinearLayout中,然后再用一个LinearLayout包裹两个水平LinearLayout以及两个文本标签控件。
平时别这样做
这样看起来并不好,对吧?前面我们谈到了扁平结构的重要性,所以正确的做法是使用ConstrainLayout作为容器。
每一个item都是LinearLayout的话开销很大的,其实我们只不过是要显示字体大小不同的文本而已,也许还要加点边框啥的,我们可以做到每个item只有一个TextView控件。
Spannables
Spannables可以让文本以不同的大小显示,而边框我们可以用drawable解决。这样一来一个item我们就节省了3个view,远比第一种方法要好。
使用Spannables的问题在于time item跟 date item的大号字符个数是不同的。
使用Databinding以及它的BindingAdapters机制,我们可以创建一个属性来设置大号字符的个数。
layout_form_order_step2.xml
<TextView android:id="@+id/txt_time1" style="@style/Widget.DateTime" app:spanOffset="@{3}" /> <TextView android:id="@+id/txt_date1" style="@style/Widget.DateTime" app:spanOffset="@{2}"/>
OrderDialogFragment.java
@BindingAdapter("app:spanOffset") public static void setItemSpan(View v, int spanOffset) { final String itemText = ((TextView) v).getText().toString(); final SpannableString sString = new SpannableString(itemText); sString.setSpan(new RelativeSizeSpan(1.75f), itemText. length() - spanOffset, itemText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ((TextView) v).setText(sString); }
Scenes(场景)
也许实现下面这个概念的本质是如何实现book按钮和确认视图之间的过渡效果。
如果我们利用好障眼法的话,这也并不难。如果我们不把按钮视作按钮,而是视作一个容器,我们就会意识到可以使用scenes来做到这个动画效果。
整个表单为第一个scene,确认视图则为第二个scene,按钮则是两个界面的共享元素。共享元素就是两个scene之间所共同拥有的一个view,framework将使用一个transition为这个view做恰当的动画。
一旦我们知道逻辑就是改变两个scenes,问题就简单了。
OrderDialogFragment.java
private void changeToConfirmScene() { final LayoutOrderConfirmationBinding confBinding = prepareConfirmationBinding(); final Scene scene = new Scene(binding.content, ((ViewGroup) confBinding.getRoot())); scene.setEnterAction(onEnterConfirmScene(confBinding)); final Transition transition = TransitionInflater .from(getContext()).inflateTransition( R.transition.transition_confirmation_view); TransitionManager.go(scene, transition); }
效果
参考
-
ConstraintLayout Documentation - Android developers
-
Mastering android drawables - Cyril Mottier
-
Efficient android layouts - Dan Lew
-
Plaid - Nick Butcher