MultipleS | Android —— 从零开始写一款开源项目

989 阅读12分钟

前言

话说在全球最大开源网站(也就是你们常说的全球最大同性交友网站)Github混迹许久,学习和见识到了许多热心网友和组织的开源。在一边暗叹为什么别人可以做到,难道自己就不可以吗,虽然不一定做的好,但是至少要尝试嘛。男子汉大丈夫,放开手脚去干就好了。说干就干,刚好最近在学习自定义View,就选择从这个方面切入吧

开始之前

马上就大四了,压力越来越大,写这个的初衷是对自己的一项锻炼,后来想能不能给点提示给同样是学习中的人一点帮助。因为我这个项目是关于自定义View的,但是也不需要有太强的自定义View能力,如果你是老手了,可以直接退出了,同时如果你愿意看下去,不管是指点还是批评,我都乐意和乐于接收。如果你和我一样,也是想学习学习自定义View,那么说不定这边项目对你或多或少有点帮助。在开始之前可以推荐你几篇我觉得好的关于自定义View的文章。(建议按顺序食用)

MultipleS

此项目是一款多选性的选择控件。类似京东的地址选择器,在其效果基础上进行扩展,并可以添加各种各样的数据源进行显示。设计灵感来源京东和天猫(真的不是我喜欢网购)

以下所有内容均只介绍项目中一种方案,完整项目最后会给出地址

目标

  • [x] Tab效果类似TabLayout&&内容列表RecyclerView
  • [x] Tab和列表都是可点击的 效果类似京东地址选择
  • [x] 多种布局方式(Tab横向和纵向,内容列表线性和网格)
  • [x] 数据自定义,根据不同数据显示不同内容

其实要达到同样的效果有许多种实现方式,我的想法是尽可能的复用,既然是做开源,那就要用心做,做到最简单最轻便是首要要素。其次效果尽可能的符合目前Google推荐的Material Design。

实现过程

首先大家可以先自己思考思考你想用那些View或者ViewGroup来实现这样的效果。方法肯定有很多种,如果你想的和我不同,或者觉得比我的更加方便,可以告诉我,也可以试试自己的思路。思考和动手才是前进的基础。
那么接下来我跟大家分享分享我的思路
其实我是在网上也看了许多资料。再根据自己的想法去实现的。顶部的Tab使用自定义View,中间的滑动线条使用自定义LinearLayout,列表使用RecyclerView
初步设计如下:


注意:以下所有的属性设置方法应该在show()之前调用,而setDataSource()/setImgSource()方法则应该在show()方法之后调用。(最好是在项目下New Module 编写)

TabView

顶部的Tab主要效果是显示文字,可以设置的属性有文字选中时的颜色、文字未选择时的颜色、文字大小、以及Tab状态。

public class TabView extends TextView {
    /**.....*/
    private void initView(){
        setTextSize(tabTextSize);
    }

    @Override
    public void setSelected(boolean selected) {
        isSelected = selected;
        setText(getText());
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        if (isSelected){
            setTextColor(isSelectedColor);
        }else {
            setTextColor(unSelectedColor);
        }
        super.setText(text, type);
    }

    public void  resetTxt(){
        setText("");
    }

    public void resetStatus(){
        isSelected=false;
        setTextColor(unSelectedColor);
    }
    public void setTabTextSize(int tabTextSize) {
        this.tabTextSize = tabTextSize;
        //更改文字大小
        initView();
    }

    @Override
    public boolean isSelected() {
        return isSelected;
    }
    /**.....*/
}

IndicatorView

本来当初滑动线条我是打算直接单纯的用自定义View的,后来自己做的时候发现不好控制其长度和形态(能力不够啊~),后来在郭神的一篇微信文章上看到有使用自定义LinearLayout实现方便多了,于是也拿来学习。首先我们要实现的效果类似TabLayout的滑动线条的效果,所以这里要设置动画滑动效果,线条颜色,控制线条长度。还要就是设置Tab的横纵方向,这里只贴出横向走向的方法,更多可以看源码:

    private void initHorizontalView(Context context) {
        viewOrientation = 0;
        setOrientation(HORIZONTAL);
        setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 6));
        setWeightSum(sum);
        view = new View(context);
        view.setLayoutParams(new LayoutParams(0, LayoutParams.MATCH_PARENT, 1));
        view.setBackgroundColor(bgColor);
        addView(view);
    }

    //滑动效果
    public void scrollView(int to) {
        this.toPage = to;
        int width = getWidth() / sum;
        int height = getHeight() / sum;
        ObjectAnimator animator;
        if (viewOrientation == 0) {
            animator = ObjectAnimator.ofFloat(view, TRANSLATION_X, view.getTranslationX(), (toPage - fromPage) * width);
            animator.setDuration(400);
            animator.start();
        } else if (viewOrientation == 1) {
            animator = ObjectAnimator.ofFloat(view, TRANSLATION_Y, view.getTranslationY(), (toPage - fromPage) * height);
            animator.setDuration(400);
            animator.start();
        }
    }

顶部的Tab选择器组件就已经出来了,还是比较简单的。接下来就是将它们组合起来实现我们最终的效果

MultipleSelector

这个类就是我们主要的实现类了,代码有点多,我就个别分析一些方法吧

private TabView addTab(CharSequence text, boolean isSelected) {
        TabView tab = new TabView(mContext);
        if (tabOrientation == VERTICAL) {
            tab.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
            tab.setPadding(tabPadding/2, tabPadding/2, tabPadding/2, tabPadding/2);
        } else {
            tab.setLayoutParams(new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1));
            tab.setPadding(0, tabPadding, 0, tabPadding);
        }
        tab.setIsSelectedColor(tabIsSelectedColor);
        tab.setUnSelectedColor(tabUnSelectedColor);
        tab.setTabTextSize(tabTextSize);
        tab.setGravity(Gravity.CENTER);
        tab.setSelected(isSelected);
        tab.setText(text);
        tab.setOnClickListener(this);
        return tab;
    }

首先是addTab()方法,顾名思义就是添加TabView进我们的选择器,并设置给其相应的属性,没有难度,那么继续往下看

private void initHorizontalView(Context context) {
        removeAllViews();
        setOrientation(VERTICAL);
        tabGroup = new LinearLayout(context);
        tabGroup.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        tabGroup.setWeightSum(tabCount);
        tabGroup.setOrientation(HORIZONTAL);
        addView(tabGroup);

        TabView tab = addTab(tabTextHint, true);
        tabGroup.addView(tab);
        tabList = new ArrayList<>();
        tabList.add(tab);
        for (int i = 1; i < tabCount; i++) {
            TabView nullTab = addTab("", false);
            nullTab.setIndex(i);
            tabGroup.addView(nullTab);
            tabList.add(nullTab);
        }

        indicatorView = new IndicatorView(context, tabCount);
        indicatorView.setBgColor(indicatorColor);
        indicatorView.setTabOrientation(HORIZONTAL);
        indicatorView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 6));
        addView(indicatorView);

        divisionLine = new View(context);
        divisionLine.setLayoutParams(new LayoutParams(
                LayoutParams.MATCH_PARENT, 2));
        divisionLine.setBackgroundColor(context.getResources().getColor(R.color.gray_line));
        addView(divisionLine);

        recyclerView = new RecyclerView(context);
        recyclerView.setLayoutManager(manager);
        if (manager instanceof GridLayoutManager){
            recyclerView.addItemDecoration(new SpaceItemDecoration(space));
        }
        LayoutParams layoutParams=new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        layoutParams.setMargins(10,10,10,0);
        recyclerView.setLayoutParams(layoutParams);
        addView(recyclerView);


    }

这就是主要的加载视图的方法了,首先设置了整个选择器的横纵走向,然后依次添加Tab列表,滑动线条和内容列表,并设置好对应的自定义属性。需要注意的是要处理好每部分的大小,不然整个效果就很难看了

/**
     * 设置列表布局方式
     *
     * @param manager
     */
    public void setManager(RecyclerView.LayoutManager manager) {
        this.manager = manager;
        if (manager instanceof GridLayoutManager) {
            isLinear = false;
            spanCount=((GridLayoutManager) manager).getSpanCount();
        }
    }

设置内容列表的布局方式,默认是线性布局的,这里我们取出spanCount是用来使用GridLayoutManager的时候处理网格之间的间距

    //设置列表数据
    public void setDataSource(ArrayList list) {
        if (list == null || list.size() <= 0)
            return;
        if (list.get(0) instanceof DataSourceInterface) {
            this.dataList = list;
            if (adapter != null) {
                adapter.notifyDataSetChanged();
            }else {
                adapter = new MultipleSelectorAdapter();
                recyclerView.setAdapter(adapter);
            }
        } else {
            throw new RuntimeException("DataSource must implement DataSourceInterface");
        }
    }

    //如果是网格布局 可以设置图片数据
    public void setImgSource(ArrayList list){
        if (list==null || list.size()==0)
            return;
        this.imgList=list;
        if (adapter!=null){
            adapter.notifyDataSetChanged();
        }else {
            adapter=new MultipleSelectorAdapter();
            recyclerView.setAdapter(adapter);
        }
    }

前面设计需求的时候,我们的目标是可以让使用者自定义内容数据,在这里我经过了一阵子头脑风暴,如果内容是线性列表那么我想实现效果类似京东地址选择器应该是最佳的,但是如果是网格列表,效果则会十分丑陋,所以我的想法是网格列表的时候实现效果为类似天猫超市分类的效果,所有我提高一个设置图片url的方法

/**
     * 获取当前所有选中Item的值
     * @return
     */
    public List<String> getAllTabText(){
        ArrayList<String> allTabTextList=new ArrayList<>();
        for (int i = 0; i < tabList.size()-1; i++) {
            if (!(tabList.get(i).getText().toString()).equals(tabTextHint)){
                allTabTextList.add(tabList.get(i).getText().toString());
            }
        }
        return allTabTextList;
    }

    /**
     * 获取当前所有选中Item的对应的下标
     * @return
     */
    public List<Integer> getAllTabPosition(){
        ArrayList<Integer> allTabPositionList=new ArrayList<>();
        for (int i = 0; i < tabList.size()-1; i++) {
            if (!(tabList.get(i).getText().toString()).equals(tabTextHint)){
                allTabPositionList.add(i);
            }
        }
        return allTabPositionList;
    }

既然是选择器,那么肯定是需要获取到最终选择的值的或者是列表中的下标。

private void resetAllTabs(int tabIndex) {
        if (tabList != null) {
            for (int i = 0; i < tabList.size(); i++) {
                tabList.get(i).resetStatus();
                if (i > tabIndex) {
                    tabList.get(i).resetTxt();
                }
            }
        }
    }


    private void resetAllTabStatus() {
        if (tabList != null) {
            for (int i = 0; i < tabList.size(); i++) {
                tabList.get(i).resetStatus();
                if (i == tabIndex) {
                    tabList.get(i).setStatus(tabList.get(i));
                }
            }
        }
    }

    @Override
    public void onClick(View v) {
        TabView tab = (TabView) v;
        if (TextUtils.isEmpty(tab.getText()))
            return;
        tabIndex = tab.getIndex();
        resetAllTabStatus();
        if (onTabItemSelectedListener != null) {
            if (tab.isSelected())
                onTabItemSelectedListener.TabItemReSelected(this, tab);
            else
                onTabItemSelectedListener.TabItemSelected(this, tab);
        }
        indicatorView.scrollView(tabIndex);
        tab.setSelected(true);
        Log.i(TAG, "tabIndex=" + tabIndex);
    }

设置顶部Tab的点击事件,效果类似京东,需要注意的是,如果当前tab是再次点击的,并且重新点击了Item的话,是要重置后面所有的Tab的

MultipleSelectorAdapter
同样只抽取部分代码,主要分析Item的点击事件,其他内容可以查看源码

holder.itemView.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (onListItemSelectedListener != null) {
                            onListItemSelectedListener.ListItemSelected(MultipleSelector.this, (DataSourceInterface) v.getTag(), tabIndex,position);
                            tabList.get(tabIndex).setText(((DataSourceInterface) v.getTag()).getTextName());
                            tabList.get(tabIndex).setTag(v.getTag());
                            if (tabIndex + 1 < tabList.size()) {
                                tabIndex += 1;
                                resetAllTabs(tabIndex);//重制所有tab状态
                                indicatorView.scrollView(tabIndex);
                                tabList.get(tabIndex).setSelected(true);//设置当前tab为选中状态
                                tabList.get(tabIndex).setText(tabTextHint);
                            }
                        }
                    }
                });

我们需要点击Item后显示所点击Item的值到当前tab上,设置好对应的状态,并加载下一个tab。

使用

做完了?对,做完了,但是具体效果怎么样呢。拿出来遛一遛就知道了,首先在布局文件中添加我们的MultipleSelector:

    <com.swy.multipleselector.MultipleSelector
        android:id="@+id/select"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

然后在Activity中:

MultipleSelector selector= (MultipleSelector) findViewById(R.id.select);
        selector.setManager(new LinearLayoutManager(context));
        selector.show();
        selector.setDataSource(cities1);
        selector.setOnListItemSelectedListener(new OnListItemSelectedListener() {
            @Override
            public void ListItemSelected(MultipleSelector selector, DataSourceInterface dataSourceInterface, int tabPosition, int position) {
                switch (tabPosition){
                    case 0:
                        cities2= (ArrayList<Address.CitylistBean.CBean>) cities1.get(position).getC();
                        selector.setDataSource(cities2);
                        break;
                    case 1:
                        cities3= (ArrayList<Address.CitylistBean.CBean.ABean>) cities2.get(position).getA();
                        selector.setDataSource(cities3);
                        break;
                    case 2:
                        Toast.makeText(context,"tabPosition :"+tabPosition+" "+dataSourceInterface.getTextName(),Toast.LENGTH_SHORT).show();
                        break;
                    default:
                        break;
                }
            }
        });
        selector.setOnTabItemSelectedListener(new OnTabItemSelectedListener() {
            @Override
            public void TabItemSelected(MultipleSelector selector, TabView tabView) {
                switch (tabView.getIndex()){
                    case 0:
                        selector.setDataSource(cities1);
                        break;
                    case 1:
                        selector.setDataSource(cities2);
                        break;
                    case 2:
                        selector.setDataSource(cities3);
                        break;
                    default:
                        break;
                }
            }

            @Override
            public void TabItemReSelected(MultipleSelector selector, TabView tabView) {
                switch (tabView.getIndex()){
                    case 0:
                        selector.setDataSource(cities1);
                        break;
                    case 1:
                        selector.setDataSource(cities2);
                        break;
                    case 2:
                        selector.setDataSource(cities3);
                        break;
                    default:
                        break;
                }
            }
        });

看上去你可能会说,怎么还是有这么一大串代码,我都还没设置各种属性呢。其实大部分代码都是设置数据源的操作,说来也头疼,我既想让使用者自定义数据源,目前又没有很好的思路去简化这个过程。如果你有,麻烦指教我。万分感谢为了方便使用,我内部写好了一个默认的仿京东地址选择的效果,你只需要在代码中这样写:

MultipleSelector selector= (MultipleSelector) findViewById(R.id.select);
selector.userDefaultSelector(this,selector);

最后效果来张图:


注意:所有的属性设置方法应该在show()之前调用,而setDataSource()/setImgSource()方法则应该在show()方法之后调用。

如果你想了解更多内容,欢迎到我的Github查看完整项目!

发布开源项目到Jcenter/Bintray

如果你使用过一些开源项目,你会发现我们只需要在项目中添加一行代码就可以了,比如:

compile 'com.swy:multipleselector:1.0.0'

不得不说相对以前自己导入的方式方便了我们许多,那么怎么做到方便别人呢,接下来就知道了。

1、Jcenter/Bintray

2、配置项目gradle和local.properties

1、配置Project.gradle文件
项目一般会有多个gradle配置文件,第一步要配置的是项目的gradle,也就是Project.gradle文件

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'

        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0' //添加此行

        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1' //添加此行
    }
}

2、配置Module.gradle文件

apply plugin: 'com.android.library'
//添加如下两行
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'

def siteUrl = 'https://github.com/yeshuwei/MultipleS' //项目在github主页地址
def gitUrl = 'https://github.com/yeshuwei/MultipleS.git'   //Git仓库的地址

group = "com.swy"//发布aar前缀根节点

version = "1.0.0"//发布aar的库版本

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"

    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
install {
    repositories.mavenInstaller {
        // This generates POM.xml with proper parameters
        pom {
            project {
                packaging 'aar'
                name 'MutipleS'//添加项目描述
                url siteUrl
                licenses {
                    license {
                        name 'The Apache Software License, Version 2.0'
                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                developers {
                    developer {
                        id 'xxx'//设置自己ID
                        name 'xxx'//设置自己名字
                        email 'xxx@gmail.com'//设置自己邮箱
                    }
                }
                scm {
                    connection gitUrl
                    developerConnection gitUrl
                    url siteUrl
                }
            }
        }
    }
}
task sourcesJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs
    classifier = 'sources'
}

task javadoc(type: Javadoc) {
    source = android.sourceSets.main.java.srcDirs
    classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
    failOnError false
}

task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier = 'javadoc'
    from javadoc.destinationDir
}

artifacts {
    archives javadocJar
    archives sourcesJar
}

Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
bintray {
    user = properties.getProperty("bintray.user")
    key = properties.getProperty("bintray.apikey")
    configurations = ['archives']
    pkg {
        repo = "maven" // 上传到maven库。(这里要特别注意,如果写了maven报404错误,请在bintray创建一个仓库,这里填改成你创建的仓库的名字
        name = "MultipleS" //项目在JCenter的名字
        websiteUrl = siteUrl
        vcsUrl = gitUrl
        licenses = ["Apache-2.0"]
        publish = true
    }
}

3 、配置local.properties文件
上传的开源项目一般会托管到github上,我们上传的时候会把项目和module的gradle文件传上去,所有不能将账号密码(apikey)直接写到gradle文件中,而我们的local.properties文件一般不会上传(没经验的人可能会传),所以我们把用户隐私信息配置到local.properties, 在gradle中动态读取

sdk.dir=你的sdk路径
# 添加下面两行,第一个填你的用户名,第二个是你的ApiKey
bintray.user=swy
bintray.apikey=************************

3、上传项目到Jcenter

  • 这里如果你系统配置了gradle的用户环境,在Android Studio下的Terminal输入gradle install,如果没有配置gradle用户环境,输入gradlew install,如果没有问题,最终你会看到BUILD SUCCESSFUL。
  • 如果你看到了生成javadoc时编译不过,那么要看下在gradle中task javadoc下有没有failOnError false这句话,在刚才编写gradle时提示过了。如果加了这句而你的javadoc写的不规范会有警告,你不用鸟它。
  • 最后一步,运行gradle install后看到BUILD SUCCESSFUL后,再输入上传命令gradle bintrayUpload,等一分钟左右就执行完了,会提示SUCCESSFUL。
  • 浏览器bintray.com/后会看到你的项目。

这个时候你可以通过以下配置使用了:

Project:Grade

  allprojects {
    repositories {
        maven { url 'https://dl.bintray.com/swy/multipleS' } //添加该行
    }
}
Moudle:Grade
    compile 'com.swy:multipleselector:1.0.0'

但是如果你想只用一行代码的话,还需要Bintray的管理员审核。在刚才上传的项目页面点击查看详情,点击Add to Jcetner:


基本24小时之内都是可以通过审核的。坐等就好了

因为篇幅原因这部分内容我没有做仔细的介绍,可能你还有部分疑问,如果你想了解什么是Jcenter/Bintray可以读读这篇文章
如果你想了解上传的详细过程,推荐你 这篇文章

最后

整个项目我只简述了部分代码,如果你觉得想深入了解,可以查看源码。
项目地址请收下,欢迎提出意见和您宝贵的star。学习自定义的过程我感觉十分痛苦,需要掌握的细节很多,并且需要时间去熟练。总而言之,既要多动动脑子,也要多动手,实践出真知嘛