如今,要实现导航功能方案有很多。比如:

  1、用3.0+自带的Toolbar + Fragment导航。

  2、用Tabhost实现导航。小弟学浅,就只用过这两种方案实现导航。

  但是这两种方案都有一个很明显的弊端:导航的位置太过于固定了。比如Toolbar的就只能在标题栏处(ps:源码修改大神跳过)。还有Tabhost,虽然自定义Tabhost比直接继承TabActivity更加灵活,但是却没有选项切换动画(ps:也许是我没发现)。

  

  有时候,我们仅仅是想在一个画面的一角处,贴上一个导航,用于切换导航啊,属性设置之内的。这个时候不管是Toolbar还是Tabhost都有些大材小用,心有余而力不足的感觉了。比如下图所示:

最近刚好项目有这方面的需要,就查了点资料。发现原理其实挺简单的,如下图:

上面几个tab用Button或者TextView来做就行,反正能响应点击就行。下面的ImageView用于切换动画,比如默认是tab1,这个时候点击了tab3,那么下面的ImageView就从tab1移动到tab3并且停留。

原理讲明白之后,接下来就是具体的实现了,一般这类需要都能有两种方式实现:

  1. 用xml中实现
  2. 用java代码动态实现。

Xml界面与java代码控制分离是Android开发的亮点,也是无数入门书籍的敲门砖,但是这种实现就有一种非常大的局限性:今天这个项目有3个tab,明天的项目有4个tab,这个时候需要去改xml不说,还要去改一些底层实现,比如对ImageView的宽度的压缩等等。为了移植性和拓展性,我选择了java代码实现,直接subClass LinearLayout来实现。我只做了一些基本的操作,大家可以在我的代码上添加自己的操作,比如给每一个tab添加selector,添加事件回调等等。先上图,我的最精简实现:

 

中间的移动是有动画效果的哈,不是直接点哪儿就出现在哪儿,太生硬了。

接下来讲解具体的实现过程:

  1. 子类化LinearLayout,当然也可以选择子类化其他ViewGroup,看个人爱好。

public class CustomMenu extends LinearLayout implements OnClickListener

  1. 在attrs.xml文件中申明自定义xml属性

<?xml version="1.0" encoding="utf-8"?>

<resources>

<declare-styleable name="CustomMenu">

<attr name="buttonNumber" format="integer" />

<attr name="indexbitmap" format="reference" />

<attr name="buttonHeight" format="dimension" />

</declare-styleable>

</resources>

其中buttonNumber:导航的tab个数

indexbitmap:移动的图片,就是下面那一横线

buttonHeight:导航的高度

  1. 在xml布局文件添加布局,layout_width与layout_height可以随意使用match_parent、wrap_content、或者限定dp

<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"

xmlns:custommenu="http://schemas.android.com/apk/res/com.example.fragmentdemo"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical" >

<TextView

android:layout_width="match_parent"

android:layout_height="30dp"

android:text="@string/hello_world" />

<com.example.fragmentdemo.fragmentmenu.CustomMenu

android:layout_width="match_parent"

android:layout_height="0dp"

android:layout_weight="1"

custommenu:buttonNumber="5"

custommenu:buttonHeight="40dp"

custommenu:indexbitmap="@drawable/a"

>

</com.example.fragmentdemo.fragmentmenu.CustomMenu>

<TextView

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:text="@string/hello_world" />

</LinearLayout>

为了突出随意性,故意在上下添加了两个TextView,layout_width与layout_height可以设置为match_parent、wrap_content或者30dp等等。在xml属性中,我将导航栏的数量设置为5个,导航栏的高度为40dp,导航的移动图片为drawable

  1. 在java代码中首先读取自定义的xml属性值

private void readXML(Context context,AttributeSet attr){

TypedArray a = context.obtainStyledAttributes(attr, R.styleable.CustomMenu);

//读取按钮数量

buttonNumber = a.getInt(R.styleable.CustomMenu_buttonNumber, 4);

//读取按钮的高度

buttonHeight = (int) a.getDimension(R.styleable.CustomMenu_buttonHeight, 30);

//读取图片

int bitmapID = a.getResourceId(R.styleable.CustomMenu_indexbitmap, R.drawable.a);

bitmap = BitmapFactory.decodeResource(getResources(), bitmapID);

bitmap_width = bitmap.getWidth();

a.recycle();

}

注释已经写得很清楚了,就是用来读取在xml中自定义的属性,这儿注意buttonNumber、buttonHeight、bitmap、bitmap_width都是成员属性。

  1. 添加几个tab,个数是根据buttonNumber限制了的,还有tab的高度也是根据buttonHeight限制了的。

//设置本身为竖直方向

setOrientation(LinearLayout.VERTICAL);

//添加一个横向的LinearLayout,高度为设置的高度

LayoutParams p = new LayoutParams(LayoutParams.MATCH_PARENT, buttonHeight);

LinearLayout linearLayout = new LinearLayout(context);

linearLayout.setOrientation(LinearLayout.HORIZONTAL);

linearLayout.setPadding(0, 0, 0, 0);

linearLayout.setGravity(Gravity.CENTER);

addView(linearLayout, p);

//向这个横向的LinearLayout添加指定个Button

LayoutParams btn_p = new LayoutParams(LayoutParams.MATCH_PARENT, buttonHeight, 1);

for(int i = 0;i<buttonNumber;i++){

Button button = new Button(context);

button.setText("按钮"+i);

button.setTextSize(15);

button.setBackgroundColor(getResources().getColor(R.color.defaultColor));

button.setId(ID+i);

button.setOnClickListener(this);

//添加到容器

button_container.add(button);

//添加到布局

linearLayout.addView(button, btn_p);

}

这儿首先添加一个横向的LinearLayout用来添加tab,高度用用户输入的值,然后添加用户指定数量的tab(Button),设置权重(weight)为1。在这儿我把Button的文字、背景颜色等都给了默认值,大家可以在xml中拓展,或者在代码中暴露方法让用户设置。这儿有一个ID,我给了默认值

private static final int ID = 0xcc33cc;

这是为了区分onClick事件,大家可以自己选择区分方式,不过在这里用ID是有好处的,后面我会介绍。

  1. 添加ImageView,暂时不做处理,因为Bitmap要因为tab的宽度来动态调整

imageView = new ImageView(context);

LayoutParams iv_p = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

iv_p.setMargins(0, 5, 0, 0);

addView(imageView,iv_p);

这里只是放置一个ImageView,具体的内容要等到后面设置,因为内容是动态的,在构造函数期间不能确定其宽高。

  1. 在onMearsure方法中,获取本View的宽度与高度

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

//只执行这个方法一次

if(width==0 || height==0){

//得到自身的高度与高度

width = MeasureSpec.getSize(widthMeasureSpec);

height = MeasureSpec.getSize(heightMeasureSpec);

//做其他的初始化

initial();

}

}

大家都知道,onMeasure方法会根据传入的参数确定控件的大小。一般在这个方法做控件的动态伸缩和子控件的伸缩。在这里,我只是简单的得到了本控件的宽度和高度。Width和height都是成员变量。这里用了if语句是因为这个方法默认会执行两次,原因呢大概是作为ViewGroup刚开始会绘制一次,填充子控件后又会绘制一次,具体的不太清楚,大家可以查查其他资料。这里用if限定只执行一次。然后在initial()方法中,做剩下的初始化部分。

  1. 做一些初始化操作

//如果图片的宽度比按钮的宽度大,则对图片进行处理

if(bitmap_width>width/buttonNumber){

//缩小图片

bitmap = dealBitmap(bitmap, (float) (width)/buttonNumber/bitmap_width);

}

private Bitmap dealBitmap(Bitmap bitmap ,float bili) {

Matrix matrix = new Matrix();

matrix.postScale(bili, bili); // 长和宽放大缩小的比例

Bitmap resizeBmp = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix,true);

return resizeBmp;

}

如果图片的宽度大于了每一个tab的宽度,那么就对图片进行缩放,默认是缩放到与tab等同宽度。大家可以自己定义这个缩放范围。甚至可以通过设置回调接口暴露给外部设置。

//设置偏移值

imageView_offset = (width/buttonNumber-bitmap_width)/2;

//设置图片

imageView.setImageBitmap(bitmap);

//初始化图片位置

initialImageViewOffset();

设置偏移值,这个imageView_offset也是成员变量。目的是让ImageView放在tab的正中间。原理类似于下图:

然后设置bitmap,因为这个时候bitmap的宽高已经确定了。然后调用initialImageViewOffset()方法将刚才确定的offset的值设置进去

private void initialImageViewOffset() {

//偏移值大于0则进行图片移动

if(imageView_offset>0){

Matrix matrix = new Matrix();

matrix.postTranslate(imageView_offset, 0);

imageView.setImageMatrix(matrix);

}

}

这里对offset进行的大于0的判断,因为如上所说,如果bitmap的宽度大于tab的宽度,那么就需要缩放到和tab一样大,这个时候offset自然等于0,就避免了无用功。

  1. 添加单击事件

public void onClick(View v) {

//从当前项移动到点击项

moveImageView(cur_index, v.getId()-ID);

//赋值当前项

cur_index = v.getId() - ID;

}

这里就可以看出用id区分tab的好处了。为了更方便,首先贴出moveImageView的代码

private void moveImageView(int start,int end){

//要移动的距离

int length = (2 * imageView_offset + bitmap_width) * (end - start);

//初始位置,默认的ImageView在离左边的imageView_offset处。

int offset = (2 * imageView_offset + bitmap_width)*start;

Animation animation = new TranslateAnimation(offset, offset + length, 0, 0);

//动画结束后,View停留在结束的位置

animation.setFillAfter(true);

animation.setDuration(300);

imageView.startAnimation(animation);

}

这里的start是指的当前的tab编号,编号是从0开始的,比如tab0、tab1、tab2。

end是指的你点击的tab编号。例如一开始我就点了tab3,那么start=0,end=3。然后我又点了tab2,那么start=3,end=2。

其中length是指要移动的距离,如下图表示:

稍微观察就能看出

int length = (2 * imageView_offset + bitmap_width) * (end - start);

这个关系式。并且包含了正负的情况哦。

Offset是指的动画开始的x坐标,因为刚才默认移动了一个imageView_offset,所以每次的初始x坐标的公式为如下,也相当容易看出来

int offset = (2 * imageView_offset + bitmap_width)*start;

然后就是启动动画效果,从offset移动到offset+length处,并设置setFillAfter(true);方法让控件在动画停止后停留在结束的地方。

大家要注意动画与matrix的区别,动画只做了视觉效果的移动,实际的ImageView并没有移动。而Matrix是移动的ImageView

再返回看onClick方法,

public void onClick(View v) {

//从当前项移动到点击项

moveImageView(cur_index, v.getId()-ID);

//赋值当前项

cur_index = v.getId() - ID;

}

cur_index是成员变量,初始化为0,大家也可以自己设置初始值,也可以设置其他tab为默认项,不过要是这样做还有两个地方需要修改:

1、initialImageViewOffset你需要将ImageView初始化到当前的tab下。

2、moveImageView方法中的offset的计算公式也要改变。

因为每一个tab的id是通过ID+i变量的形式设置的,这儿可以直接通过v.getId()-ID的方式找到对应的tab。这个就是我所说的方便快捷的原因。

这个只是最简单的实现,大家可以丰富下,比如给button添加selector啊,添加text和监听回调啊、在下面空余的布局添加自定义的布局啊。等等等发挥的余地。对了,因为是导航,别往了添加ViewPager哦。这里由于时间关系就不写了,大家自己补充吧。

Android开发之自定义局部导航菜单的更多相关文章

  1. android开发之自定义组件

    android开发之自定义组件 一:自定义组件: 我认为,自定义组件就是android给我们提供的的一个空白的可以编辑的图片,它帮助我们实现的我们想要的界面,也就是通过自定义组件我们可以把我们要登入的 ...

  2. html自定义垂直导航菜单(多级导航菜单,去掉font-awesome图标,添加自己的箭头图标)

    这次在原先html自定义垂直导航菜单的基础上做了比较大的改动: 1.去掉了font-awesome图标,上级菜单右边的箭头是自己用css写的,具体参考<css三角箭头>. 2.去掉了初始化 ...

  3. html自定义垂直导航菜单(加强版--自定义传入menu参数,支持JSONArray、JSArray、JSONObject、JSObject)

    在上一篇中我简单写了个html自定义垂直导航菜单,缺点很明显,里面的数据是固定死的,不能动态更改数据. 这里我重写了一个修改版的垂直二级导航菜单,将原先的menuBox.init(config);修改 ...

  4. html自定义垂直导航菜单

    html自定义垂直导航菜单(目前只支持上级+下级两级菜单) 由于工作的需要,昨天花了三个多小时的事件整理了一份关于垂直导航二级菜单,可以通过js配置的方式初始化菜单box(测试环境:chrome 49 ...

  5. Android开发之自定义组件和接口回调

    说到自定义控件不得不提的就是接口回调,在Android开发中接口回调用的还是蛮多的.在这篇博客开始的时候呢,我想聊一下iOS的自定义控件.在iOS中自定义控件的思路是继承自UIView, 在UIVie ...

  6. Android开发之自定义的ListView(UITableViewController)

    Android开发中的ListView, 顾名方法思义,就是表视图.表示图在iOS开发中就是TableView.两者虽然名称不一样,但是其使用方法,使用场景以及该控件的功能都极为相似,都是用来展示大量 ...

  7. Android开发:仿美团下拉列表菜单,帮助类,复用简单

    近期在项目中须要用到下拉菜单.公司比較推崇美团的下拉菜单,于是要实现该功能.想着.这个功能应该是一个常常会用到的.于是何不写一个帮助类,仅仅要往这个类里面传入特定的參数,既能够实现下来菜单,并且还能够 ...

  8. Android开发进阶——自定义View的使用及其原理探索

    在Android开发中,系统提供给我们的UI控件是有限的,当我们需要使用一些特殊的控件的时候,只靠系统提供的控件,可能无法达到我们想要的效果,这时,就需要我们自定义一些控件,来完成我们想要的效果了.下 ...

  9. Android开发之自定义圆角矩形图片ImageView的实现

    android中的ImageView只能显示矩形的图片,这样一来不能满足我们其他的需求,比如要显示圆角矩形的图片,这个时候,我们就需要自定义ImageView了,其原理就是首先获取到图片的Bitmap ...

随机推荐

  1. SQL server Profiler 监视数据库活动

    做网站后台开始时需要考虑后台对数据库的一些操作,比如尽量减少查询次数,尽快释放连接,只选取必须的字段等等.如果是用三层开发,复杂的项目中直接拼装SQL语句多一点,拼装的SQL语句可以直接在SQL se ...

  2. ELK环境搭建完整说明

    ELK环境搭建完整说明 ELK:ElasticSerach.Logstash.Kibana三款产品名称的首字母集合,用于日志的搜集和搜索.简单地理解为我们可以把服务端的日志(nginx.tomcat等 ...

  3. Codeforces675D(SummerTrainingDay06-J)

    D. Tree Construction time limit per test:2 seconds memory limit per test:256 megabytes input:standar ...

  4. Codeforces339D(SummerTrainingDay06-A 线段树)

    D. Xenia and Bit Operations time limit per test:2 seconds memory limit per test:256 megabytes input: ...

  5. Hadoop Mapreduce 参数 (一)

    参考 hadoop权威指南 第六章,6.4节 背景 hadoop,mapreduce就如MVC,spring一样现在已经是烂大街了,虽然用过,但是说看过源码么,没有,调过参数么?调过,调到刚好能跑起来 ...

  6. php+xml有什么用

    很多招聘网上找php程序员的时候都说要懂xml,这个xml+php在web网站开发方面到底有什么应用呢,希望有知道的朋友能给我具体说说,谢谢了! 我说的是在网站中的实际应用有哪些,不是网上抄的xml的 ...

  7. OSGI企业应用开发(九)整合Spring和Mybatis框架(二)

    上篇文章中,我们完成了在OSGI应用中整合Spring和Mybatis框架的准备工作,本节我们继续Spring和Mybatis框架的整合. 一.解决OSGI整合Spring中的Placeholder问 ...

  8. <Android 基础(三十一)> ObjectAnimator

    简介 ObjectAnimator,是ValueAnimator的子类,支持利用目标视图的属性来实现动画效果.构造ObjectAnimator的时候,将会提取对应的参数来定义动画对象和对象属性.合适的 ...

  9. sql语句查找某一列的值得最大值。

    记录一下:sql语句查找某一列的值得最大值. 1.例如我要查找 表A中a列的最大值: 查找语句可以这么写: "select Max(a) a from A" 2.查找表A中a列中包 ...

  10. aop 拦截含有特定注解的类

    1.功能点:使用aop拦截含有自定义注解的类 1.自定义注解 package com.zhuanche.common.dingdingsync; import java.lang.annotation ...