从概念设计到安卓实现, 第一部分

gz8113 7年前
   <p>多亏了 Dribbble 和  MaterialUp 这样的设计平台,我们这些开发者才有机会接触到大量的概念设计资源。尽管如此,有时候有些细节几乎是不可能实现的,部分用户体验并没有被考虑。</p>    <p>鉴于此,我觉得建立一个这样的项目会比较有意思:选择一些 Dribbble 或者  MaterialUp 上的设计资源,在安卓上实现它们,然后撰写一些列的文章来讲解实现的细节以及我认为比较重要的安卓界面实现技巧。</p>    <h2>概念设计</h2>    <p>这是我为第一部分选择的概念设计,简单但是足以涵盖一些有趣的话题了,比如 ConstraintLayout &  chains ,  DataBinding ,UI层次结构性能的重要性以及 scenes(场景) 。</p>    <ul>     <li> <p>该概念设计的作者是 Johnyvino ,发布在  MaterialUp 上。</p> </li>     <li> <p>其实现可以在 GitHub的 这个仓库 上找到。</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d58b6cf70a7ac2b308c35b6fc1ceda7e.gif"></p>    <p>让我们开始吧!</p>    <h3>支持库中的Bottom sheets</h3>    <p>在演示图中, bottom sheet 被用来引导用户完成购买过程。在安卓中你可以找到几个第三方库来实现这种视图,比如 Umano的 AndroidSlidingUpPanel library 。</p>    <p>其实最近 bottoms sheets被引入了 design support library ,因此我决定在这里使用它,学习该如何使用。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e9ae70afec8caf12a9c1d9d69283c721.gif"></p>    <p>Bottom sheets有两种使用方式,一种是Bottom sheets作为main view 的一部分(在 CoordinatorLayout 中的一个viewgroup上使用 BottomSheetBehavior ),另一种是模态对话框似的Bottom sheets,使用 BottomSheetDialogFragment 来实现。</p>    <p>对于我们的例子我选择 BottomSheetDialogFragment ,因为这里的信息是以模态的形式显示的。这种Bottom sheets实现的代码和 DialogFragment 的用法类似。</p>    <p>HomeActivity.java</p>    <pre>  <code class="language-java">OrderDialogFragment.newInstance(    fakeProducts.get(position))      .show(getSupportFragmentManager(),null);</code></pre>    <p>OrderDialogFragment.java</p>    <pre>  <code class="language-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();  }</code></pre>    <h2>ConstraintLayout</h2>    <p>Google I/O '16上发布的这个强大的布局控件最近迎来了它的稳定版本(1.0.2)。</p>    <p>这个控件的最大好处在于,它可以让你以最扁平的层次,构造复杂的,自适应的布局。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8bf69e833a30f3bbd69e987bbd0541eb.png"></p>    <p>通常建议安卓的布局中要避免使用层次很深的视图结构,因为这样会降低性能,而且会增加UI绘制到屏幕的时间。而如果使用 ConstraintLayout ,这样的要求自动就满足了。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7150e8582b82745839e66a2c04598e63.png"></p>    <p>基本地, ConstraintLayout 的工作原理类似 RelativeLayout ,即定义视图与屏幕之间的关系。但是 ConstraintLayout 除了性能更好之外,在Android Studio的图形编辑器中的表现也真的很好。</p>    <p>除此之外还有更多有趣的机制比如view之间的chain(不同的view相互约束),或者guidelines。</p>    <h3>chain的使用</h3>    <p>假设我们有两个view,A和B,分别被约束在屏幕的右边沿和左边沿。如果A设定在B的左边,而B设定在A的右边,这样这组view就会被特殊处理,在约束布局中这样的情况被称之为chain。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d2abfbf664424196ead70b6191855dc5.png"></p>    <p>使用Android Studio的布局编辑器的上下文菜单很容易就可以创建一个chain。默认创建的是spread chain,即视图均匀分布。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ee2e876b0ac0c58e151e1c307a0f2de2.gif"></p>    <h3>Chain 类型</h3>    <p><img src="https://simg.open-open.com/show/c2dc17d242f376144281ca6ed1902d96.png"></p>    <p style="text-align:center">在这个例子中,我们只使用spread 和 packed chain,但是你可以根据自己的需要选择,最近谷歌对 <a href="/misc/goto?guid=4959742249874880803" rel="nofollow,noindex">ConstraintLayout</a> 的文档增加了对 <a href="/misc/goto?guid=4959742249964226553" rel="nofollow,noindex">chain</a> 的说明,没有理由不用。 <img src="https://simg.open-open.com/show/792ea26771aacc5b038fa04c708eb35c.png"></p>    <h2>被选中view的过渡动画</h2>    <p>这看起来比较简单,每当用户点击一个产品参数的时候,被选择的view就过渡到左下角的标签旁边。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/656ff5dc419e39ae83107a943ba9e9fc.gif"></p>    <p>为此我们在被点击的的view的parent中添加一个新的view。</p>    <p>OrderDialogFragment.java</p>    <pre>  <code class="language-java">private void transitionSelectedView(View v) {      final View selectionView = createSelectionView(v);        binding.mainContainer.addView(selectionView);        startCloneAnimation(selectionView, getTargetView(v));  }</code></pre>    <p>然后使用 TransitionManager 的 beginDelayedTransition 来做过渡动画。这个方法将检测布局是否有变化,如果有,使用传入的transition来执行动画。</p>    <p>OrderDialogFragment.java</p>    <pre>  <code class="language-java">private void startCloneAnimation(View clonedView, View targetView) {      clonedView.post(() -> {          TransitionManager.beginDelayedTransition(              (ViewGroup) binding.getRoot(), selectedViewTransition);            // 触发transition          clonedView.setLayoutParams(SelectedParamsFactory              .endParams(clonedView, targetView));      });  }</code></pre>    <h2>ViewSwitcher</h2>    <p>ViewSwitcher ,Android SDK一个并不十分流行的控件,它可以用进入和退出动画来切换两个view。这和我们购买过程中在代表两个步骤的布局之间切换是完全符合的。</p>    <p>fragment_order_form.xml</p>    <pre>  <code class="language-java"><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></code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/85b107478cf273db4e1e1182c4088bc5.gif"></p>    <p>OrderDialogFragment.java</p>    <pre>  <code class="language-java">private void showDeliveryForm() {      binding.switcher.setDisplayedChild(1);      initOrderStepTwoView(binding.layoutStep2);  }</code></pre>    <h2>Databinding(数据绑定)</h2>    <p>这个设计的实现我使用了 Databinding ,对我来说在一个对象中包含所有的布局结构是可以接受的,但是Databading还有其它一些机制值得一提。</p>    <h3>节省click listener的书写次数</h3>    <p>因为这些布局有大量的listener需要处理,我决定为用 Databinding 绑定一个listener对象来处理onClick。在xml中,为每个view的onClick属性设置方法名,然后事件就会定向到listener对象的相应方法中。</p>    <p>layout_form_order_step1.xml</p>    <pre>  <code class="language-java"><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}"      /></code></pre>    <p>OrderDialogFragment.java</p>    <pre>  <code class="language-java">layoutStep1Binding.setListener(new Step1Listener() {      @Override      public void onSizeSelected(View v) {          // ...      }        @Override      public void onColorSelected(View v) {          // ...      }  })</code></pre>    <h2>使用Spannables + databinding + ConstraintLayout来减少view</h2>    <p>让我们花点时间来思考如何实现下面这部分设计</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b9ac39b9c1d85aee8fa5404cdb516816.png"></p>    <p>一种办法是每个item设置一个垂直的LinearLayout,然后3个item被包含在一个水平的LinearLayout中,然后再用一个LinearLayout包裹两个水平LinearLayout以及两个文本标签控件。</p>    <p>平时别这样做</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b5e37f5caccd389630e3fb6ee8b02668.png"></p>    <p>这样看起来并不好,对吧?前面我们谈到了扁平结构的重要性,所以正确的做法是使用 ConstrainLayout 作为容器。</p>    <p>每一个item都是LinearLayout的话开销很大的,其实我们只不过是要显示字体大小不同的文本而已,也许还要加点边框啥的,我们可以做到每个item只有一个TextView控件。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/362276c27a3341abadfcf3ea932c8403.png"></p>    <h2>Spannables</h2>    <p>Spannables 可以让文本以不同的大小显示,而边框我们可以用drawable解决。这样一来一个item我们就节省了3个view,远比第一种方法要好。</p>    <p>使用 Spannables 的问题在于time item跟 date item的大号字符个数是不同的。</p>    <p>使用 Databinding 以及它的 BindingAdapters 机制,我们可以创建一个属性来设置大号字符的个数。</p>    <p>layout_form_order_step2.xml</p>    <pre>  <code class="language-java"><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}"/></code></pre>    <p>OrderDialogFragment.java</p>    <pre>  <code class="language-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);  }</code></pre>    <h2>Scenes(场景)</h2>    <p>也许实现下面这个概念的本质是如何实现book按钮和确认视图之间的过渡效果。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5a93c616a90761685f6729371000a0b7.gif"></p>    <p>如果我们利用好障眼法的话,这也并不难。如果我们不把按钮视作按钮,而是视作一个容器,我们就会意识到可以使用scenes来做到这个动画效果。</p>    <p>整个表单为第一个scene,确认视图则为第二个scene,按钮则是两个界面的共享元素。共享元素就是两个scene之间所共同拥有的一个view,framework将使用一个transition为这个view做恰当的动画。</p>    <p>一旦我们知道逻辑就是改变两个scenes,问题就简单了。</p>    <p>OrderDialogFragment.java</p>    <pre>  <code class="language-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);  }</code></pre>    <h2>效果</h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2f6495fccda872619357ff12e36d4256.gif"></p>    <h2>参考</h2>    <ul>     <li> <p><a href="/misc/goto?guid=4959739280310014617" rel="nofollow,noindex">ConstraintLayout Documentation</a> - Android developers</p> </li>     <li> <p><a href="/misc/goto?guid=4959742250085254008" rel="nofollow,noindex">Mastering android drawables</a> - Cyril Mottier</p> </li>     <li> <p><a href="/misc/goto?guid=4959742250173669602" rel="nofollow,noindex">Efficient android layouts</a> - Dan Lew</p> </li>     <li> <p><a href="/misc/goto?guid=4958970698499233651" rel="nofollow,noindex">Plaid</a> - Nick Butcher</p> </li>    </ul>    <p> </p>    <p>来自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0317/7692.html</p>    <p> </p>