實現Instagram的Material Design概念設計

jopen 9年前發布 | 31K 次閱讀 Android開發 移動開發 Material Design

幾個月前(這篇文章的日期是2014 年11月10日),google發布了app和web應用的Material Design設計準則之后,設計師Emmanuel Pacamalan在油Tube上發布了一則概念視頻,演示了Instagram如果做成Material風格會是什么樣子:

這 僅僅是停留在圖像上的設計,是美好的愿景,估計很多人都會問,能否使用相對簡單的辦法將它實現出來呢?答案是:yes,不僅僅能實現,而且無須要求在 Lillipop版本,實際上幾年前4.0發布之后我們就可以實現這些效果了。ps 讀到這里我們應該反思這幾年開發者是不是都吃屎去了。

鑒于這個原因,我決定開始撰寫一個新的課題-如何將INSTAGRAM with Material Design 視頻中的效果轉變成現實。當然,我們并不是真的要做一個Instagram應用,只是將界面做出來而已,并且盡量減少一些不必要的細節。

開始

本文將要實現的是視頻中前7秒鐘的效果。我覺得對于第一次嘗試來說已經足夠了。我想要提醒諸位的是,里面的實現方法不僅僅是能實現,也是我個人最喜歡的實現方式。還有,我不是一個美工,因此項目中的所有圖片是直接從網上公開的渠道獲取的。(主要是從resources page)。

好了,下面是最終效果的兩組截圖和視頻(很短的視頻,就是那7秒鐘的效果,可以在上面的視頻中看到,這里因為沒法直接引用油Tube的視頻就略了)(分別從Android 4 和5上獲得的):

實現Instagram的Material Design概念設計實現Instagram的Material Design概念設計

準備

在我們的項目中,將使用一些熱門的android開發工具和庫。并不是所有這些東西本篇文章都會用到,我只是將它們準備好以備不時之需。

初始化項目

首先我們需要創建一個新的android項目。我使用的是Android Studio和gradle的build方式。最低版本的sdk是15(即Android 4.0.4)。然后我們將添加一些依賴。沒什么好講的,下面是build.gradle以及app/build.gradle文件的代碼:

build.gradle

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.14.0'
        classpath 'com.jakewharton.hugo:hugo-plugin:1.1.+'
    }
}
 
allprojects {
    repositories {
        jcenter()
    }
}

app/build.gradle

apply plugin: 'com.android.application'
apply plugin: 'hugo'
 
android {
    compileSdkVersion 21
    buildToolsVersion "21.1"
     
    defaultConfig {
        applicationId "io.github.froger.instamaterial"
        minSdkVersion 15
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
    }
}
 
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
     
    compile "com.android.support:appcompat-v7:21.0.0"
    compile 'com.android.support:support-v13:21.+'
    compile 'com.android.support:support-v4:21.+'
    compile 'com.android.support:palette-v7:+'
    compile 'com.android.support:recyclerview-v7:+'
    compile 'com.android.support:cardview-v7:21.0.+'
    compile 'com.jakewharton:butterknife:5.1.2'
    compile 'com.jakewharton.timber:timber:2.5.0'
    compile 'com.非死book.rebound:rebound:0.3.6'
}

 

簡而言之,我們有如下工具:

一些兼容包(CardView, RecyclerView, Palette, AppCompat)-我喜歡使用最新的控件。當然你完全可以使用ListView Actionbar甚至View/FrameView來替代,但是為什么要這么折騰?��

ButterKnife - view注入工具簡化我們的代碼。(比方說不再需要寫findViewById()來引用view,以及一些更強大的功能)。

Rebound - 我們目前還沒有用到,但是我以后肯定會用它。這個非死book開發的動畫庫可以讓你的動畫效果看起來更自然。

TimberHugo - 對這個項目而言并不是必須,我僅僅是用他們打印log。

圖片資源

本項目中將使用到一些Material Design的圖標資源。App 圖標來自于NSTAGRAM with Material Design 視頻,這里complete bunch of images 是項目的全套資源。

樣式

我們從定義app的默認樣式開始。同時為Android 4和5定義Material Desing樣式的最簡單的方式是直接繼承Theme.AppCompat.NoActionBar 或者 Theme.AppCompat.Light.NoActionBar主題。為什么是NoActionBar?因為新的sdk中為我們提供了實現Actionbar功能的新模式。本例中我們將使用Toolbar控件,基于這個原因-Toolbar是ActionBar更好更靈活的解決方案。我們不會深入講解這個問題,但你可以去閱讀android開發者博客AppCompat v21


根據概念視頻中的效果,我們在AppTheme中定義了三個基本顏色(基色調):

styles.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- styles.xml-->
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/style_color_primary</item>
        <item name="colorPrimaryDark">@color/style_color_primary_dark</item>
        <item name="colorAccent">@color/style_color_accent</item>
    </style>
</resources>

colors1.xml

<?xml version="1.0" encoding="utf-8"?>
<!--colors.xml-->
<resources>
    <color name="style_color_primary">#2d5d82</color>
    <color name="style_color_primary_dark">#21425d</color>
    <color name="style_color_accent">#01bcd5</color>
</resources>

關于這三個顏色的意義,你可以在這里找到Material Theme Color Palette documentation

布局

項目目前主要使用了3個主要的布局元素

  • Toolbar - 包含導航圖標和applogo的頂部bar

  • RecyclerView - 用于顯示feed

  • Floating Action Button - 一個實現了Material Design中action button pattern的ImageButton。

在開始實現布局之前,我們先在res/values/dimens.xml文件中定義一些默認值:

<?xml version="1.0" encoding="utf-8"?>
<!--dimens.xml-->
<resources>
    <dimen name="btn_fab_size">56dp</dimen>
    <dimen name="btn_fab_margins">16dp</dimen>
    <dimen name="default_elevation">8dp</dimen>
</resources>

 這些值的大小是基于Material Design設計準則中的介紹。

現在我們來實現MainActivity中的layout:

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary">
     
        <ImageView
            android:id="@+id/ivLogo"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:scaleType="center"
            android:src="@drawable/img_toolbar_logo" />
    </android.support.v7.widget.Toolbar>
     
    <android.support.v7.widget.RecyclerView
        android:id="@+id/rvFeed"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/toolbar"
        android:scrollbars="none" />
     
    <ImageButton
        android:id="@+id/btnCreate"
        android:layout_width="@dimen/btn_fab_size"
        android:layout_height="@dimen/btn_fab_size"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_marginBottom="@dimen/btn_fab_margins"
        android:layout_marginRight="@dimen/btn_fab_margins"
        android:background="@drawable/btn_fab_default"
        android:elevation="@dimen/default_elevation"
        android:src="@drawable/ic_instagram_white"
        android:textSize="28sp" />
 
</RelativeLayout>

以上代碼的解釋:

  • 關于Toolbar最重要的特征是他現在是activity layout的一部分,而且繼承自ViewGroup,因此我們可以在里面放一些UI元素(它們將利用剩余空間)。本例中,它被用來放置logo圖片。同時,因為Toolbar是比Actionbar更靈活的控件,我們可以自定義更多的東西,比如設置背景顏色為colorPrimary(否則Toolbar將是透明的)。

  • RecyclerView雖然在xml中用起來非常簡單,但是如果java代碼中沒有設置正確,app是不能啟動的,會報java.lang.NullPointerException。


  • Elevation(ImageButton中)屬性不兼容api21以前的版本。所以如果我們想做到Floating Action Button的效果需要在Lollipop和Lollipop之前的設備上使用不同的background。

Floating Action Button

為了簡化FAB的使用,我們將用對Lollipop以及Lollipop之前的設備使用不同的樣式:

FAB for Android v21:

 實現Instagram的Material Design概念設計
FAB for Android pre-21

實現Instagram的Material Design概念設計

我們需要創建兩個不同的xml文件來設置button的background:/res/drawable-v21/btn_fab_default.xml(Lollipop設備) ,/res/drawable/btn_fab_default.xml(Lollipop之前的設備):

btn_fab_default2.xml

<?xml version="1.0" encoding="utf-8"?>
<!--drawable-v21/btn_fab_default.xml-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/fab_color_shadow">
    <item>
    <shape android:shape="oval">
    <solid android:color="@color/style_color_accent" />
    </shape>
    </item>
</ripple>

btn_fab_default1.xml

<?xml version="1.0" encoding="utf-8"?>
<!--drawable/btn_fab_default.xml-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="false">
        <layer-list>
            <item android:bottom="0dp" android:left="2dp" android:right="2dp" android:top="2dp">
                <shape android:shape="oval">
                    <solid android:color="@color/fab_color_shadow" />
                </shape>
            </item>
             
            <item android:bottom="2dp" android:left="2dp" android:right="2dp" android:top="2dp">
                <shape android:shape="oval">
                    <solid android:color="@color/style_color_accent" />
                </shape>
            </item>
        </layer-list>
    </item>
    <item android:state_pressed="true">
        <shape android:bottom="2dp" android:left="2dp" android:right="2dp" android:shape="oval" android:top="2dp">
        <solid android:color="@color/fab_color_pressed" />
        </shape>
    </item>
</selector>

上面的代碼涉及到兩個顏色的定義,在res/values/colors.xml中添加:

<color name="btn_default_light_normal">#00000000</color>
<color name="btn_default_light_pressed">#40ffffff</color>

 

可以看到在 21之前的設備商制造陰影比較復雜。不幸的是在xml中達到真實的陰影效果沒有漸變方法。其他的辦法是使用圖片的方式,或者通過java代碼實現(參見creating fab shadow)。

Toolbar

現在我們來完成Toolbar。我們已經有了background和應用的logo,現在還剩下navigation以及menu菜單圖標了。關于navigation,非常不幸的是,在xml中app:navigationIcon=""是不起作用的,而android:navigationIcon=""又只能在Lollipop上有用,所以只能使用代碼的方式了:

toolbar.setNavigationIcon(R.drawable.ic_menu_white);

注:app:navigationIcon=""的意思是使用兼容包appcompat的屬性,而android:navigationIcon=""是標準的sdk屬性。

至于menu圖標我們使用標準的定義方式就好了:

在res/menu/menu_main.xml中

<!--menu_main.xml-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".MainActivity">
        <item
            android:id="@+id/action_inbox"
            android:icon="@drawable/ic_inbox_white"
            android:title="Inbox"
            app:showAsAction="always" />
</menu>

在activity中inflated這個menu:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
}

 本應運行的很好,但是正如我在推ter上提到的,Toolbar onClick  selectors有不協調的情況:


為了解決這個問題,需要做更多的工作,首先為menu item創建一個自定義的view

res/layout/menu_item_view.xml:

<?xml version="1.0" encoding="utf-8"?>
<!--menu_item_view.xml-->
<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="?attr/actionBarSize"
    android:layout_height="?attr/actionBarSize"
    android:background="@drawable/btn_default_light"
    android:src="@drawable/ic_inbox_white" />

 

然后為Lollipop和Lollipop之前的設備分別創建onClick的selector,在Lollipop上有ripple效果:

btn_default_light2.xml            

<?xml version="1.0" encoding="utf-8"?>
<!--drawable-v21/btn_default_light.xml-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/btn_default_light_pressed" />

btn_default_light1.xml

<?xml version="1.0" encoding="utf-8"?>
<!--drawable/btn_default_light.xml-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/btn_default_light_normal" android:state_focused="false" android:state_pressed="false" />
    <item android:drawable="@color/btn_default_light_pressed" android:state_pressed="true" />
    <item android:drawable="@color/btn_default_light_pressed" android:state_focused="true" />
</selector>

現在,工程中的所有的color應該是這樣子了:

colors.xml            

<?xml version="1.0" encoding="utf-8"?>
<!--colors.xml-->
<resources>
<color name="style_color_primary">#2d5d82</color>
<color name="style_color_primary_dark">#21425d</color>
<color name="style_color_accent">#01bcd5</color>
 
<color name="fab_color_pressed">#007787</color>
<color name="fab_color_shadow">#44000000</color>
 
<color name="btn_default_light_normal">#00000000</color>
<color name="btn_default_light_pressed">#40ffffff</color>
</resources>

 最后我們應該將custom view放到menu item中,在onCreateOptionsMenu()中:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_main, menu);
    inboxMenuItem = menu.findItem(R.id.action_inbox);
    inboxMenuItem.setActionView(R.layout.menu_item_view);
    return true;
}

 以上就是toolbar的所有東西。并且onClick的按下效果也達到了預期的效果:

實現Instagram的Material Design概念設計

Feed

Last thing we should implement is feed, built onRecyclerView. Right now we have to setup two things: layout manager (RecyclerView has to know how to arrange items) and adapter (to provide items).

First thing is straightforward - while our layout is simple ListView we can useLinearLayoutManagerfor items arragement. For the second one we have to do more work, buth there is no magic to deal with.

Let’s start from defining list item layout (res/layout/item_feed.xml):

最后需要實現的是feed,基于RecyclerView實現。我們需要設置兩個東西:layout manager和adapter,因為這里其實就是想實現ListView的效果,所以直接用LinearLayoutManager就行了,而adapter我們首先從item的布局開始(res/layout/item_feed.xml):

item_feed.xml

<?xml version="1.0" encoding="utf-8"?><!-- item_feed.xml -->
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:id="@+id/card_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_margin="8dp"
    card_view:cardCornerRadius="4dp">
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
     
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_feed_top" />
         
        <io.github.froger.instamaterial.SquaredImageView
            android:id="@+id/ivFeedCenter"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
         
        <ImageView
            android:id="@+id/ivFeedBottom"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
     
    </LinearLayout>
</android.support.v7.widget.CardView>

FeedAdapter也非常簡單:

FeedAdapter.java

public class FeedAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private static final int ANIMATED_ITEMS_COUNT = 2;
     
    private Context context;
    private int lastAnimatedPosition = -1;
    private int itemsCount = 0;
     
    public FeedAdapter(Context context) {
        this.context = context;
    }
     
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        final View view = LayoutInflater.from(context).inflate(R.layout.item_feed, parent, false);
        return new CellFeedViewHolder(view);
    }
     
    private void runEnterAnimation(View view, int position) {
        if (position >= ANIMATED_ITEMS_COUNT - 1) {
            return;
        }
         
        if (position > lastAnimatedPosition) {
            lastAnimatedPosition = position;
            view.setTranslationY(Utils.getScreenHeight(context));
            view.animate()
            .translationY(0)
            .setInterpolator(new DecelerateInterpolator(3.f))
            .setDuration(700)
            .start();
        }
    }
     
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
        runEnterAnimation(viewHolder.itemView, position);
        CellFeedViewHolder holder = (CellFeedViewHolder) viewHolder;
        if (position % 2 == 0) {
            holder.ivFeedCenter.setImageResource(R.drawable.img_feed_center_1);
            holder.ivFeedBottom.setImageResource(R.drawable.img_feed_bottom_1);
        } else {
            holder.ivFeedCenter.setImageResource(R.drawable.img_feed_center_2);
            holder.ivFeedBottom.setImageResource(R.drawable.img_feed_bottom_2);
        }
    }
     
    @Override
    public int getItemCount() {
        return itemsCount;
    }
     
    public static class CellFeedViewHolder extends RecyclerView.ViewHolder {
        @InjectView(R.id.ivFeedCenter)
        SquaredImageView ivFeedCenter;
        @InjectView(R.id.ivFeedBottom)
        ImageView ivFeedBottom;
         
        public CellFeedViewHolder(View view) {
        super(view);
            ButterKnife.inject(this, view);
        }
    }
     
    public void updateItems() {
        itemsCount = 10;
        notifyDataSetChanged();
    }
}

沒什么特別之處需要說明。

通過以下方法將他們放在一起:

private void setupFeed() {
    LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
    rvFeed.setLayoutManager(linearLayoutManager);
    feedAdapter = new FeedAdapter(this);
    rvFeed.setAdapter(feedAdapter);
}

下面是整個MainActivity class的源碼:

//MainActivity.java
public class MainActivity extends ActionBarActivity {
    @InjectView(R.id.toolbar)
    Toolbar toolbar;
    @InjectView(R.id.rvFeed)
    RecyclerView rvFeed;
     
    private MenuItem inboxMenuItem;
    private FeedAdapter feedAdapter;
     
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.inject(this);
         
        setupToolbar();
        setupFeed();
    }
     
    private void setupToolbar() {
        setSupportActionBar(toolbar);
        toolbar.setNavigationIcon(R.drawable.ic_menu_white);
    }
     
    private void setupFeed() {
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        rvFeed.setLayoutManager(linearLayoutManager);
        feedAdapter = new FeedAdapter(this);
        rvFeed.setAdapter(feedAdapter);
    }
     
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        inboxMenuItem = menu.findItem(R.id.action_inbox);
        inboxMenuItem.setActionView(R.layout.menu_item_view);
        return true;
    }
}

運行結果:

Android Lollipop

實現Instagram的Material Design概念設計

Android pre-21

實現Instagram的Material Design概念設計

動畫

最后一件也是最重要的事情就是進入時的動畫效果,再瀏覽一遍概念視頻,可以發現在main Activity啟動的時候有如下動畫,分成兩步:


顯示Toolbar以及其里面的元素

在Toolbar動畫完成之后顯示feed和floating action button。

Toolbar中元素的動畫表現為在較短的時間內一個接一個的進入。實現這個效果的主要問題在于navigation icon的動畫,navigation icon是唯一一個不能使用動畫的,其他的都好辦。

Toolbar animation

首先我們只是需要在activity啟動的時候才播放動畫(在旋轉屏幕的時候不播放),還要知道menu的動畫過程是不能在onCreate()中去實現的(我們在onCreateOptionsMenu()中實現),創建一個布爾類型的變量pendingIntroAnimation,在onCreate()方法中初始化:

//...
if (savedInstanceState == null) {
    pendingIntroAnimation = true;
}

onCreateOptionsMenu():

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_main, menu);
    inboxMenuItem = menu.findItem(R.id.action_inbox);
    inboxMenuItem.setActionView(R.layout.menu_item_view);
    if (pendingIntroAnimation) {
        pendingIntroAnimation = false;
        startIntroAnimation();
    }
    return true;
}

這樣startIntroAnimation()將只被調用一次。

現在該來準備Toolbar中元素的動畫了,也非常簡單

ToolbarAnimation

//...
private static final int ANIM_DURATION_TOOLBAR = 300;
 
private void startIntroAnimation() {
    btnCreate.setTranslationY(2 * getResources().getDimensionPixelOffset(R.dimen.btn_fab_size));
    int actionbarSize = Utils.dpToPx(56);
    toolbar.setTranslationY(-actionbarSize);
    ivLogo.setTranslationY(-actionbarSize);
    inboxMenuItem.getActionView().setTranslationY(-actionbarSize);
     
    toolbar.animate()
        .translationY(0)
        .setDuration(ANIM_DURATION_TOOLBAR)
        .setStartDelay(300);
    ivLogo.animate()
        .translationY(0)
        .setDuration(ANIM_DURATION_TOOLBAR)
        .setStartDelay(400);
    inboxMenuItem.getActionView().animate()
        .translationY(0)
        .setDuration(ANIM_DURATION_TOOLBAR)
        .setStartDelay(500)
        .setListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
            startContentAnimation();
            }
        })
        .start();
}
//...

在上面的代碼中:

  • 首先我們將所有的元素都通過移動到屏幕之外隱藏起來(這一步我們將FAB也隱藏了)。

  • 讓Toolbar元素一個接一個的開始動畫

  • 當動畫完成,調用了startContentAnimation()開始content的動畫(FAB和feed卡片的動畫)

 簡單,是吧?

Content 動畫

在這一步中我們將讓FAB和feed卡片動起來。FAB的動畫很簡單,跟上面的方法類似,但是feed卡片稍微復雜些。

startContentAnimation方法

//...
//FAB animation
private static final int ANIM_DURATION_FAB = 400;
 
private void startContentAnimation() {
    btnCreate.animate()
        .translationY(0)
        .setInterpolator(new OvershootInterpolator(1.f))
        .setStartDelay(300)
        .setDuration(ANIM_DURATION_FAB)
        .start();
    feedAdapter.updateItems();
}
//...

FeedAdapter的代碼在上面已經貼出來了。結合著就知道動畫是如何實現的了。


本篇文章就結束了,避免遺漏,這里是這篇文章是提交的代碼commit for our project with implemented animations.

 

源代碼

完整的代碼在Github repository.

作者: Miroslaw Stanek


英文原文:Instagram with Material Design concept is getting real 

轉載請注明出處:http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0204/2415.html 

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!