Android UI框架快速搭建實踐

轉載請注明原作者,如果你覺得這篇文章對你有幫助或啟發,可以關注打賞。

ui框架搭建.gif

如上圖,本文主要講解2點 (mvp和dagger2不是本文重點):

  • 基類的抽取和封裝(mvp+Dagger2)
  • 如何使用ViewPager+TabLayout快速搭建ios風格的多個底部導航欄的主頁框架

別問我為什么不來個MaterialDesign風格的,說起來都是淚,我個人是喜歡MD的,可以給用戶更清爽更有層次的視覺感受,但致命問題就是她直接呈現給用戶的信息少了,與當前國內的關注點有沖突,很多產品不懂MD,Android的界面開發也全按IOS來的,設計圖都是一套這種事我是不會告訴你的。

代碼最直觀,我就直接上代碼了。ps:為節省大家時間,本文只會展示核心代碼。

BaseActivity

 package com.example.arron.demo.view.base;
 import android.support.v7.app.AppCompatActivity;
 import android.view.KeyEvent;
 import com.example.arron.demo.internal.di.modules.ActivityModule;
 import com.example.arron.demo.view.navigation.Navigator;
 import javax.inject.Inject;
 /**
 * Created by Arron on 16/6/28.
 */
  public abstract class BaseActivity extends AppCompatActivity {
    //使用Dagger2注入的全局導航類
    @Inject
    public Navigator navigator;
    //動態獲取類名 打印日志使用
    protected String TAG = this.getClass().getSimpleName();

    //布局文件ID
    protected abstract int getContentViewId();

    /**
     * 布局中Fragment的ID
     * 如果沒有fragment則不必實現
     */
    protected abstract int getFragmentContentId();

    //添加fragment
    protected void addFragment(BaseFragment fragment) {
        if (fragment != null) {
            getSupportFragmentManager().beginTransaction()
                    .replace(getFragmentContentId(), fragment, fragment.getClass().getSimpleName())
                    .addToBackStack(fragment.getClass().getSimpleName())
                    .commitAllowingStateLoss();
        }
    }

    //移除fragment
    protected void removeFragment() {
        if (getSupportFragmentManager().getBackStackEntryCount() > 1) {
            getSupportFragmentManager().popBackStack();
        } else {
            finish();
        }
    }

    //返回鍵返回事件的處理
    //如果FragmentStack中只有1個fragment 關閉當前activity
    // 如果FragmentStack中還有>1數量fragment則可以removeFragment()將fragment出棧 此部分交給子類實現
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (KeyEvent.KEYCODE_BACK == keyCode) {
            if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
                finish();
                return true;
            }
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    protected void onStop() {
        super.onStop();
    }

    //配合Dagger2使用 返回當前Activity的ActivityModule對象
    // ActivityModule生命周期與activity是綁定的
    protected ActivityModule getActivityModule() {
        return new ActivityModule(this);
    }
}

AppActivity

package com.example.arron.demo.view.base;

import android.content.Intent;
import android.os.Bundle;

import com.example.arron.demo.AndroidApplication;

/**
 * Created by Arron on 16/6/29.
 */

public abstract class AppActivity extends BaseActivity {

    /**
     * 獲取第一個fragment  如果沒有返回null即可
     */
    protected abstract BaseFragment getFirstFragment();

    /**
     * 處理Intent
     *
     * @param intent
     */
    protected void handleIntent(Intent intent) {
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getContentViewId());
        AndroidApplication.getComponent().inject(this);
        if (null != getIntent()) {
            handleIntent(getIntent());
        }
        initView();
        initData();
        //避免重復添加Fragment
        if (null == getSupportFragmentManager().getFragments()) {
            BaseFragment firstFragment = getFirstFragment();
            if (null != firstFragment) {
                addFragment(firstFragment);
            }
        }
    }

    /**
     * 初始化data
     */
    protected abstract void initData();

    /**
     * 初始化view
     */
    protected abstract void initView();

}

HomeActivity

package com.example.arron.demo.view.activity;

import android.graphics.drawable.Drawable;
import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.widget.TextView;

import com.example.arron.demo.R;
import com.example.arron.demo.utils.ResourceUtils;
import com.example.arron.demo.view.adapter.HomeFragmentAdapter;
import com.example.arron.demo.view.base.AppActivity;
import com.example.arron.demo.view.base.BaseFragment;

import butterknife.Bind;
import butterknife.ButterKnife;

/**
 * Created by Arron on 16/6/29.
 */

public class HomeActivity extends AppActivity {
    @Bind(R.id.home_content)
    ViewPager container;
    @Bind(R.id.tab)
    TabLayout tab;
    private HomeFragmentAdapter adapter;

    @Override
    protected BaseFragment getFirstFragment() {
        return null;
    }

    @Override
    protected void initData() {

    }

    @Override
    protected void initView() {
        ButterKnife.bind(this);
        tab.setTabMode(TabLayout.MODE_FIXED);
        initTab();
        setListener();
        setAdapterAndNotify();
        container.setOffscreenPageLimit(3);
    }

    private void setAdapterAndNotify() {
        if (null == adapter) {
            adapter = new HomeFragmentAdapter(getSupportFragmentManager(), 4);
            container.setAdapter(adapter);
        } else {
            adapter.notifyDataSetChanged();
        }
    }

    private void setListener() {
        //這行代碼將TabLayout與ViewPager的頁面切換綁定 原理很簡單 看源碼
        container.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tab));
        tab.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                int position = tab.getPosition();
                //ViewPager切換頁面無動畫需要使用兩個參數的方法并傳入false
                container.setCurrentItem(position, false);
                //這句別忘了 否則tab就丟失選擇器效果了
                tab.getCustomView().setEnabled(true);
                //當前頁面的數據加載
                adapter.getItem(position).loadData();
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {
                //別忘了
                tab.getCustomView().setEnabled(false);
            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });

    }

    //為了達到切換tab文字和icon同步變色 這里給TextView設置選擇器使用Enabled屬性切換
    //icon同理
    private void initTab() {
        LayoutInflater inflater = getLayoutInflater();
        TextView view;
        for (int i = 0; i < 4; i++) {
            view = (TextView) inflater.inflate(R.layout.tab_home_item, null);
            String text = null;
            Drawable drawable = null;
            switch (i) {
                case 0:
                    text = ResourceUtils.getString(R.string.tab_main);
                    view.setEnabled(true);
                    drawable = ResourceUtils.getDrawable(R.drawable.tab_main);
                    break;
                case 1:
                    text = ResourceUtils.getString(R.string.tab_what);
                    drawable = ResourceUtils.getDrawable(R.drawable.tab_what);
                    break;
                case 2:
                    text = ResourceUtils.getString(R.string.tab_message);
                    drawable = ResourceUtils.getDrawable(R.drawable.tab_message);
                    break;
                case 3:
                    text = ResourceUtils.getString(R.string.tab_mine);
                    drawable = ResourceUtils.getDrawable(R.drawable.tab_mine);
                    break;
            }
            view.setText(text);
            drawable.setBounds(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight());
            view.setCompoundDrawables(null, drawable, null, null);
            TabLayout.Tab tab = this.tab.newTab().setCustomView(view);
            this.tab.addTab(tab, i == 0 ? true : false);
        }
    }

    @Override
    protected int getContentViewId() {
        return R.layout.activity_home;
    }

    @Override
    protected int getFragmentContentId() {
        return 0;
    }
}

BaseFragment

package com.example.arron.demo.view.base;

import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.example.arron.demo.presenter.Presenter;
import com.example.arron.demo.view.BaseView;
import com.example.arron.demo.view.loading.VaryViewHelperController;
import com.trello.rxlifecycle.components.support.RxFragment;

import java.util.List;

import butterknife.ButterKnife;


/**
 * Created by Arron on 16/6/29.
 */

public abstract class BaseFragment<T extends Presenter> extends RxFragment implements BaseView {
    //與Fragment綁定的activity對象
    protected BaseActivity mActivity;
    //當前View的Presenter
    protected T mPresenter;
    private View contentView;
    //通用loading頁error頁等的控制器
    private VaryViewHelperController mVaryViewHelperController;

    protected abstract void initView(View view, Bundle savedInstanceState);

    /**
     * 初始化數據 頁面加載完畢調用
     */
    protected abstract void initData();

    /**
     * 切換到頁面需要重新加載數據的實現此方法
     */
    public abstract void loadData();

    //獲取布局文件ID
    protected abstract int getLayoutId();

    //獲取宿主Activity
    protected BaseActivity getHoldingActivity() {
        return mActivity;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        this.mActivity = (BaseActivity) activity;
    }

    //添加fragment
    protected void addFragment(BaseFragment fragment) {
        if (null != fragment) {
            getChildFragmentManager().beginTransaction()
                    .replace(getFragmentContentId(), fragment, fragment.getClass().getSimpleName())
                    .addToBackStack(fragment.getClass().getSimpleName())
                    .commitAllowingStateLoss();
        }
    }

    //移除fragment
    protected void removeFragment() {
        if (getChildFragmentManager().getBackStackEntryCount() > 1) {
            getChildFragmentManager().popBackStack();
        }
    }

    //添加fragment的布局節點的ID
    protected abstract int getFragmentContentId();

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (contentView == null) {
            contentView = inflater.inflate(getLayoutId(), container, false);
            initView(contentView, savedInstanceState);
        } else {
            ViewGroup parent = (ViewGroup) contentView.getParent();
            if (parent != null) {
                parent.removeView(contentView);
            }
        }
        if (null == mVaryViewHelperController)
            mVaryViewHelperController = new VaryViewHelperController(getLoaingTargetView());
        if (null == mPresenter)
            mPresenter = getChildPresenter();
        return contentView;
    }

    protected abstract T getChildPresenter();

    protected abstract View getLoaingTargetView();

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        initData();
    }

    @Override
    public void setMenuVisibility(boolean menuVisible) {
        super.setMenuVisibility(menuVisible);
        if (null != this.getView()) {
            this.getView().setVisibility(menuVisible ? View.VISIBLE : View.INVISIBLE);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        if (null != mPresenter)
            mPresenter.resume();
    }

    @Override
    public void onPause() {
        super.onPause();
        if (null != mPresenter)
            mPresenter.pause();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        ButterKnife.unbind(this);
        if (null != mPresenter)
            mPresenter.destroy();
    }

    @Override
    public BaseActivity getContext() {
        return mActivity;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        //Google bug
        outState.putString("WORKAROUND_FOR_BUG_19917_KEY", "WORKAROUND_FOR_BUG_19917_VALUE");
        super.onSaveInstanceState(outState);
    }

    @Override
    public void showLoading() {
        if (mVaryViewHelperController == null) {
            throw new IllegalStateException("no ViewHelperController");
        }
        mVaryViewHelperController.showLoading();
    }

    @Override
    public void refreshView() {
        if (mVaryViewHelperController == null) {
            throw new IllegalStateException("no ViewHelperController");
        }
        mVaryViewHelperController.restore();
    }

    @Override
    public void showNetError() {
        if (mVaryViewHelperController == null) {
            throw new IllegalStateException("no ViewHelperController");
        }
        mVaryViewHelperController.showNetworkError(v -> {
            showLoading();
            mPresenter.requestData(getRequestParams());
        });
    }

    @Override
    public void hasNoMoreData() {

    }

    @Override
    public void loadMoreFinish(List dates) {

    }

    @Override
    public void showRefreshFinish(List score) {

    }

    @Override
    public void showToastError() {

    }

    protected String getRequestParams() {
        return null;
    }

    @Override
    public void showEmptyView(String msg) {
        if (mVaryViewHelperController == null) {
            throw new IllegalStateException("no ViewHelperController");
        }
        mVaryViewHelperController.showEmpty(msg);
    }
}

eg:具體頁面的Fragment

package com.example.arron.demo.view.fragment;

import android.os.Bundle;
import android.view.View;

import com.example.arron.demo.R;
import com.example.arron.demo.presenter.Presenter;
import com.example.arron.demo.view.base.BaseFragment;

/**
 * Created by Arron on 16/6/29.
 */
public class MineFragment extends BaseFragment {

    @Override
    protected void initView(View view, Bundle savedInstanceState) {

    }

    @Override
    protected void initData() {

    }

    @Override
    public void loadData() {

    }

    @Override
    protected int getLayoutId() {
        return R.layout.fragment_mine;
    }

    @Override
    protected int getFragmentContentId() {
        return 0;
    }

    @Override
    protected Presenter getChildPresenter() {
        return null;
    }

    @Override
    protected View getLoaingTargetView() {
        return null;
    }
}

View層使用到的VaryViewHelperController

package com.example.arron.demo.view.loading;

import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.example.arron.demo.R;


public class VaryViewHelperController {

    private IVaryViewHelper helper;

    public VaryViewHelperController(View view) {
        this(new VaryViewHelper(view));
    }

    public VaryViewHelperController(IVaryViewHelper helper) {
        super();
        this.helper = helper;
    }

    public void showNetworkError(View.OnClickListener onClickListener) {
        View layout = helper.inflate(R.layout.pager_error);
        Button againBtn = (Button) layout.findViewById(R.id.pager_error_loadingAgain);
        if (null != onClickListener) {
            againBtn.setOnClickListener(onClickListener);
        }
        helper.showLayout(layout);
    }

    public void showEmpty(String emptyMsg) {
        View layout = helper.inflate(R.layout.page_no_data);
        TextView textView = (TextView) layout.findViewById(R.id.tv_no_data);
        if (!TextUtils.isEmpty(emptyMsg)) {
            textView.setText(emptyMsg);
        }
        helper.showLayout(layout);
    }

    public void showLoading() {
        View layout = helper.inflate(R.layout.pager_loading);
        helper.showLayout(layout);
    }

    public void restore() {
        helper.restoreView();
    }
}

VaryViewHelperController中使用到的添加移除view的工具類IVaryViewHelper

package com.example.arron.demo.view.loading;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

/**
 * VaryViewHelper可以方便添加或移除view
 */
public class VaryViewHelper implements IVaryViewHelper {
    private View view;
    private ViewGroup parentView;
    private int viewIndex;
    private ViewGroup.LayoutParams params;
    private View currentView;

    public VaryViewHelper(View view) {
        super();
        this.view = view;
    }

    private void init() {
        params = view.getLayoutParams();
        if (view.getParent() != null) {
            parentView = (ViewGroup) view.getParent();
        } else {
            parentView = (ViewGroup) view.getRootView().findViewById(android.R.id.content);
        }
        int count = parentView.getChildCount();
        for (int index = 0; index < count; index++) {
            if (view == parentView.getChildAt(index)) {
                viewIndex = index;
                break;
            }
        }
        currentView = view;
    }

    @Override
    public View getCurrentLayout() {
        return currentView;
    }

    @Override
    public void restoreView() {
        showLayout(view);
    }

    @Override
    public void showLayout(View view) {
        if (parentView == null) {
            init();
        }
        this.currentView = view;
        if (parentView.getChildAt(viewIndex) != view) {
            ViewGroup parent = (ViewGroup) view.getParent();
            if (parent != null) {
                parent.removeView(view);
            }
            parentView.removeViewAt(viewIndex);
            parentView.addView(view, viewIndex, params);
        }
    }

    @Override
    public View inflate(int layoutId) {
        return LayoutInflater.from(view.getContext()).inflate(layoutId, null);
    }

    @Override
    public Context getContext() {
        return view.getContext();
    }

    @Override
    public View getView() {
        return view;
    }
}

That's all!我個人更喜歡看代碼,所以沒有大段的文字,但關鍵點都在代碼中注釋了,如果有不明白的地方,歡迎提問。

應大家的要求將代碼分享到GitHub上了,感興趣的同學可以去看看。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容