一個實用的android框架(三)—— 兼容性

原文出處:http://saulmm.github.io/a-useful-stack-on-android-2-user-interface/

原碼github地址:https://github.com/saulmm/Material-Movies

作者:Saúl Molinero

系列文章:

這是“一個實用的android架構”系列的第三章節。

第一章節中,我主要講述了一個模塊化和可拓展的架構,這個架構基於Model View Presenter (MVP)

第二章節描述在UI上對Material Design的嘗試,包括顏色,過渡,矢量圖等。

在第三章節中,我們將探討兼容性的問題。安卓的碎片化是十分嚴重的,各種各樣的版本,屏幕大小,設備特性等等。因爲這個原因,我們需要降低應用的版本(原來是Lollipop),並且我們也會嘗試適配不同的屏幕大小。

所有的例子都在GihHub上可以找到:https://github.com/saulmm/Material-Movies

降低SDK版本

我將SDK的版本定爲了16。從Google近日提供的數據可以看到,Jelly Bean(4.1)以上的安卓設備已經佔到了86.8%。(這個數據在國內成不成立還未可知)

安卓設備使用情況

爲了支持4.1以上的版本,需要多項目做一些必要的修改。例如:共享元素的過渡效果(transitions with shared element)是在5.0才被引入到安卓框架中的。

共享元素的過渡效果

當你在MoviesActivity中點擊一個電影的時候,我們就開始檢測當前的版本是不是高於或者等於Lollipop。如果是的話,那麼我們就可以使用新的API來完成這個過渡效果。如果不是的話,我使用了一個動畫達到了相似的效果。

MoviesActivity

@Override
public void onClick(View v, int position, 
    float touchedX, float touchedY) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 
        startSharedElementPosition(touchedView, position,
            movieDetailActivityIntent);

    else
        startDetailActivityAnimation(touchedView, (int) touchedX, 
            (int) touchedY, movieDetailActivityIntent);
}

因爲沒有辦法讓一個元素從一個Activity轉移到另一個Activity,所以在MovieDetailActivity中,我會讓海報從用戶點擊的位置開始放大彈出。

MovieDetailActivity.java

@Override
public void onCreate(Bundle savedInstanceState) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 
        configureEnterTransition ();

     else {

        mViewLastLocation = getIntent()
            .getIntArrayExtra("view_location");

        configureEnterAnimation ();
    }
}

...

private void configureEnterAnimation() {

    GUIUtils.startScaleAnimationFromPivot(
        mViewLastLocation[0], mViewLastLocation[1],
        mObservableScrollView, new AnimatorAdapter() {

            @Override
            public void onAnimationEnd(Animator animation) {

                super.onAnimationEnd(animation);
                GUIUtils.showViewByScale(mFabButton);
            }
        }
    );

    animateElementsByScale();
}

GuiUtils.java

public static void startScaleAnimationFromPivot (
    int pivotX, int pivotY, final View v,
    final AnimatorListener animatorListener) {

    final AccelerateDecelerateInterpolator interpolator =
        new AccelerateDecelerateInterpolator();

    v.setScaleY(SCALE_START_ANCHOR);
    v.setPivotX(pivotX);
    v.setPivotY(pivotY);

    v.getViewTreeObserver().addOnPreDrawListener(
        new OnPreDrawListener() {

            @Override
            public boolean onPreDraw() {

                v.getViewTreeObserver().removeOnPreDrawListener(this);

                ViewPropertyAnimator viewPropertyAnimator = 
                    v.animate()
                    .setInterpolator(interpolator)
                    .scaleY(1)
                    .setDuration(SCALE_DELAY);

                if (animatorListener != null)
                    viewPropertyAnimator.setListener(
                        animatorListener);

                viewPropertyAnimator.start();
                return true;
            }
        });
    }

效果圖:

效果圖

VectorDrawables和滑動過渡效果

另一個需要進行適配的是VectorDrawables。它也是在安卓5.0才被引入進來的。爲了達到相似的效果,我使用了一個星星旋轉並伸縮的動畫。這個動畫是使用ViewPropertyAnimator實現的。(譯者注:如果想要適配更低的版本,如2.1之類的,請使用NineOldAndroid來代替)

CircularReveal也是在Lollipop纔有的API。爲了實現相同的過渡效果,我將視圖從點擊位置進行了拉伸。

MovieDetailActivity.java

@Override
public void showConfirmationView() {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
        GUIUtils.showViewByRevealEffect(mConfirmationContainer,
            mFabButton, GUIUtils.getWindowWidth(this));

     else
        GUIUtils.startScaleAnimationFromPivot(
            (int) mFabButton.getX(),(int) mFabButton.getY(),
            mConfirmationContainer, null);

    animateConfirmationView();
    startClosingConfirmationView();
}

MovieDetailActivity.java

@Override
public void animateConfirmationView() {

    Drawable drawable = mConfirmationView.getDrawable();

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 
        if (drawable instanceof Animatable)
            ((Animatable) drawable).start();

    else 
        mConfirmationView.startAnimation(
            AnimationUtils.loadAnimation(this,
                R.anim.appear_rotate));
    }
}

效果圖:

效果圖

適配不同的屏幕大小

安卓支持不同的設備,設備的大小和屏幕的分辨率都各有不同。爲了讓一個應用在一個4寸的收集和一個10-12寸的平板上擁有相同的顯示效果,一定要使用下面這幾個技巧。

效果圖

AutofitRecyclerView

電影是通過RecyclerViewGridLayoutManager來以表格的形式展示的。谷歌提供了一個帶有spanCount參數的構造函數,可以用它來設置列數。

public GridLayoutManager (Context context, int spanCount)

問題就在於,我們需要根據屏幕的寬度來決定列數的多少。在GridView中,它有一個屬性android:numColumns = "auto_fit"可以解決這個問題。然而,RecyclerView並沒有這個選項。因此,我們想要達到的效果是手動實現類似的效果。

Chiu-Ki Chan已經遇到並解決了這個問題,她的解決方法也發佈到了博客中。簡單的來說,她通過屏幕的寬度來動態的設置spanCount。

效果圖:

適配效果

多種資源

資源文件在安卓體系中的地位是無可爭論的。它可以使得用戶在nexus 5上的體驗和一個10寸nexus上有很大的區別。MoviesDetailActivity上的元素就可以根據不同的屏幕顯示不同的資源。

適配效果

最佳的實現效果是使用的不同的佈局文件,使得實際的MovieDetailActivity不需要做太大的修改。下面是這個應用的資源目錄:

目錄結構

我們可以根據一下幾點來對資源文件進行區分:

  • 對於屏幕寬度小於600dp的設備,將使用沒有-w600dp的資源文件。這裏指的是大部分的收集設備,比如:nexus 5,nexus 4等等。
  • 對於屏幕寬度大於600dp的設備,將被分成3中:-w600dp,-w600dp-land和-w600dp-port。

根據這樣一種分類,我們可以將佈局文件和Dimen文件放到不同的目錄中,以使得應用可以根據不同的屏幕去找到合適的資源。

另外一個問題是VectorDrawable是在SDK 21之後的版本才被引入到安卓體系中的,因此需要放置到-v21的目錄下。例如,使用VectorDrawable顯示星星的ImageView在Lollipop和較低的版本中是不同的。

activity_detail.xml (通用)

<FrameLayout>
    <!-- awesome hidden code -->

    <include
        layout="@layout/imageview_star"
    />

</FrameLayout>

imageview_star.xml (layout-v21)

<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_detail_confirmation_image"
    android:layout_width="300dp"
    android:layout_height="300dp"
    android:layout_gravity="center"
    android:src="@drawable/avd_star"
    />

imageview_star.xml (layout)

<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_detail_confirmation_image"
    android:layout_width="150dp"
    android:layout_height="150dp"
    android:layout_gravity="center"
    android:src="@drawable/star"
    />

我使用了這個方案來解決適配問題,但是模塊化的資源文件提供了跟多的可能解決方案。例如,寬度和高度可以從資源中的dimension獲取,在values/dimen.xml中爲150dp,在values-v21/dimen.xml中是300dp。這個drawable可能是使用的drawable-v21/star.xml,也可能是drawable/star.xml。

視差效果

如果你觀察谷歌圖書(Google Books)應用,你可以發現在Toolbar下面有一個小的View。當你往上滑動書籍列表的時候,這個View會和Toolbar有一個滑動速度的差異,從而可以造成視差的效果。

谷歌圖書

關於這一點,Michal Z.如何在列表滑動的時候隱藏/顯示Toolbar(How to hide/show Toolbar when list is srolling)系列文章中有一個更深入的講解。

在’-w600dp-land`或更寬尺寸下的佈局文件:

 <View
        android:id="@+id/activity_movies_background_view"
        android:layout_width="match_parent"
        android:layout_height="@dimen/
        activity_movies_background_view_height"
        android:background="@color/theme_primary"
        />

這個View是通過ButterKnife注入的。假如是在nexus 5的話,佈局文件是目錄layouts中的activity_movies.xml,其中並不存在這個View。因此我們必須給這個View標記爲@Optional。

MoviesActivity.java

@Optional
@InjectView(R.id.activity_movies_background_view) 
View mTabletBackground;

MoviesActivity.java

private RecyclerView.OnScrollListener recyclerScrollListener = 
    new RecyclerView.OnScrollListener() {

    @Override
    public void onScrolled(RecyclerView recyclerView, 
        int dx, int dy) {

        // awesome hidden code here

        if (mTabletBackground != null) {

            mBackgroundTranslation = mTabletBackground
                .getY() - (dy / 2);

            mTabletBackground.setTranslationY(mBackgroundTranslation);
        }
    }

結果圖:

視差效果

譯者總結

在這一章節中,主要描述了作者在適配過程中遇到的幾個典型問題。可能適用性不是那麼廣,不過思路還是值得借鑑的。其實適配的過程往往造成了一個很尷尬的情況,即,如果用低版本的代碼邏輯可以實現的功能,爲什麼要用高版本的去實現了,爲什麼一定要存在兩套代碼呢?這樣子既不利於保證樣式統一,也給開發和修改帶來了很大的麻煩。這也就是爲什麼許多舊式的API任然盛行的緣故,API更新推廣之路也是相當的緩慢。作爲開發人員,能做到的也就是儘量跟緊時代潮流吧。

這個項目的系列博客到這裏也就完結。翻譯第一篇的時候,是覺得這個項目的架構上做得不錯,解釋也比較清楚,就翻譯了過來。後來看到閱讀量比較高,就乾脆一口氣把後續兩篇也翻譯了出來,算是有一個整體的介紹。

總得來說,這個項目不論在架構還是UI上都是屬於目前比較流行的方向,各個地方都有可取之處。因此大家可以在開發的時候,靈活使用一些代碼作爲模板,提高自己的開發效率。不過學而不思則罔,在借鑑的時候一定要去思考它的原理以及特性,以使得下次需要遇到類似問題的時候可以自己解決或者重用代碼。個人還是覺得,程序員一定要多對自己的代碼折騰,不能容許任何囉嗦和重複。只有在不斷折騰自己的過程中,才能提高自己的水平。

發佈了33 篇原創文章 · 獲贊 43 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章