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阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343

推荐阅读更多精彩内容