说到自定义控件不得不提的就是接口回调,在Android开发中接口回调用的还是蛮多的。在这篇博客开始的时候呢,我想聊一下iOS的自定义控件。在iOS中自定义控件的思路是继承自UIView, 在UIView的子类中组合一些控件,对外暴漏一些属性和回调接口,并留有必要的实现方法。在iOS自定义控件中常用的回调有两种,一是委托代理回调(Delegate),另一种是Block回调。如果你想对这两者有所了解,请参考我之前的博客《Objective-C中的委托(代理)模式》、《Objective-C中的Block回调模式》、《设计模式(十三):从“FQ”中来认识代理模式(Proxy Pattern)》。
在Android自定义控件时用到的接口回调和iOS开发中使用到的Delegate回调以及Block回调即为相似,就连实现方式都大同小异。今天的内容就自定义一个Android控件,并且以此控件为基础,聊一下Android中的接口回调(确切的说应该是Java语言中的接口回调)。废话少说,进入今天的主题。
一.自定义控件的UI实现
上面有提到,iOS开发中,自定义控件一般式继承自UIView的,然后再UIView的子类中做一些事情。而Android开发中的自定义控件也是继承自View, 但是今天我们的自定义控件是继承自FrameLayout, 在此基础上我们自定义一些东西。因为FrameLayout, LinearLayout等布局方式都是继承自ViewGroup的,而ViewGroup则继承自View, 所以在自定义控件时,继承自FrameLayout等布局方式肯定是可以的。
1. 实现效果分析
接下来我们要自定义一个导航栏,而这个导航栏是模仿iOS系统中的NavigationBar。因为Android开发中没有这个控件,所以我们需要自定义这个控件供开发者使用。下方是我们要实现的效果。上方的导航栏是我们自定义的NavigationBar,和iOS系统的导航栏类似。点击左边的返回按钮,会退出当前Activity。点击右边的借口回调测试,会通过接口回调的形式来在当前Activity中显示Toast提示。在调用该组件时,可以知道中间的Title.
2. UI布局分析以及Xml布局文件实现
接下来我们将会对UI进行拆分,详细的看一下上面的NavigationBar是由哪些基础控件组成的。分析完毕后,在通过一些布局方式将这些基础控件进行组合,拼装,最终成为我们想要使用的自定义控件。下方是手动画的上述自定义控件UI原理图,如下所示。
最下边的布局我们采用的时FrameLayout方式,并设置其背景颜色。返回图标(ImageView)和 返回文字(TextView)放在了一个水平布局的LinearLayout上。这两者上面放了一个透明的Button, 用来实现返回操作。中间的Title(TextView) 在FrameLayout中设置成居中显示即可。Call Back是一个Button, 用来测试下面的接口回调。
上面的布局格式具体落实到xml中,如下所示,具体思想就是上方叙述的东西。该自定义控件布局文件如下:
1 <?xml version="1.0" encoding="utf-8"?> 2 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="wrap_content" 5 android:background="#cccccc"> 6 <LinearLayout 7 android:orientation="horizontal" 8 android:layout_width="match_parent" 9 android:layout_height="20pt" 10 android:background="@drawable/background"> 11 <ImageView 12 android:id="@+id/back_title" 13 android:layout_width="wrap_content" 14 android:layout_height="18pt" 15 android:layout_gravity="center" 16 android:src="@drawable/back"/> 17 <TextView 18 android:layout_width="wrap_content" 19 android:layout_height="wrap_content" 20 android:layout_gravity="center" 21 android:textSize="8pt" 22 android:text="返回"/> 23 </LinearLayout> 24 25 <Button 26 android:id="@+id/back_button" 27 android:layout_width="50pt" 28 android:layout_height="20pt" 29 android:layout_gravity="center_vertical" 30 android:alpha="0" /> 31 32 <TextView 33 android:id="@+id/navigation_title" 34 android:layout_width="wrap_content" 35 android:layout_height="wrap_content" 36 android:textSize="10pt" 37 android:layout_gravity="center" 38 android:text="标题"/> 39 40 <Button 41 android:id="@+id/call_back" 42 android:layout_width="wrap_content" 43 android:layout_height="wrap_content" 44 android:layout_gravity="right" 45 android:text="接口回调测试"/> 46 </FrameLayout>
二. 为UI绑定事件以及留出回调接口
UI实现好后,就说明我们自定义组件的壳儿已经做好了,但是其内在的东西还需要实现。也就是说需要为上述实现的UI绑定Java类,并在类中处理控件的一些响应事件,以及在类中留出必要的接口来改变自定义组件的属性。接下来来实现xml对应的Java类。
因为上述布局中,最外层我们使用的是FrameLayout布局,上面已经粗略的提过,我们可以继承自FrameLayout来做一些东西,因为FrameLayout的父类是View, 所以我们可以在此基础上做一些东西。同理,如果上述布局是使用其他布局来实现的,那么你就可以继承自其他布局的类来做一些东西。在本篇博客中我们就以FrameLayout为父类来实现我们自定义组件的关联类。
1. 继承FrameLayout并实现相应的构造函数,下方是我们要实现的构造函数。在构造函数中,我们需要与上述我们实现的xml布局文件进行关联,当然,我们使用的是LayoutInflater来实现的,自定义组件的构造函数如下所示。
/* *自定义组件的构造方法 */ public CustomNavigationBar(Context context, AttributeSet attrs) { super(context, attrs); LayoutInflater.from(context).inflate(R.layout.custom_navigation, this); //加载布局文件 }
2. 实现好相应的构造方法并关联好相应的布局文件后,我们需要对布局文件中的控件进行事件的处理。下方的代码就是点击返回按钮要做的事情,因为点击返回按钮要做的事情就是结束当前Activity,所以不需要给调用者留有回调接口,在自定义组件的内部处理即可。下方代码就是获取UI中返回按钮,并处理返回事件的方法。下方的方法需要在构造函数中调用才会起作用,函数不调用怎么执行呢,对吧~。 下方代码较为简单,就是结束当前显示的Activity,处理返回按钮的事件如下:
1 /* 2 * 点击返回按钮方法 3 */ 4 private void onClickBackButton() { 5 Button button = (Button) findViewById(R.id.back_button); 6 button.setOnClickListener(new OnClickListener() { 7 @Override 8 public void onClick(View v) { 9 ((Activity) getContext()).finish(); 10 } 11 }); 12 }
3.处理好返回事件后,我们需要做的还有就是为标题栏的标题留出设置的方法。也就是说在调用该自定义组件时,我们要能设置该组件的标题。要满足这一点,我们就需要在自定义组件中留出Title的setter方法了,并且这个Setter方法的访问权限必须是Public的,不然在外界就没办法访问这个方法了。下方就是这个设置title的Public方法。其实下方的代码还是比较简单的,就是通过ID来获取标题的TextView,并设置相应的title即可,代码如下:
1 public String navigationTitle = "标题栏"; 2 3 /* 4 * 设置标题栏的标题 5 */ 6 public void setNavigationTitle(String navigationTitle) { 7 this.navigationTitle = navigationTitle; 8 TextView textView = (TextView) findViewById(R.id.navigation_title); 9 textView.setText(navigationTitle); 10 }
4. 上面如果还算简单的话,下方就是自定义控件中稍稍有点难度的地方了。接下来我们要实现相应按钮的接口回调,在实现之前我们介绍一下为什么要实现接口的回调。因为有时候点击自定义控件中的按钮时,所做的事情在自定义控件的内部无法独立完成,需要在调用者中进行事件的处理,在这种情况下,我们就可以使用接口回调来处理。
上面实现的返回事件的处理就没必要使用接口的回调了,因为在自定义组件内部完全可以该功能。举个使用接口回调的栗子:比如点击自定义控件中某个按钮时,我们需要跳转到其他Activity,而这个Activity在我们实现自定义控件时是未知的,这时候就要用到我们的接口回调来实现了。在iOS开发中,同样遇到上述问题,所以iOS开发中也有各种回调比如Block回调,Delegate回调,Target-Action回调等都是iOS开发中常用的回调。虽然实现形式不同,但是其作用和Java中的接口回调是极为相似的。好,说这么多,接下来我们要为XML布局文件中id为call_back的按钮的点击事件通过接口回调的形式传递到调用者中。
(1)第一步我们要先实现接口回调的接口,这也是必须的,因为接口回调如果没有接口怎么能行呢。该接口是Public类型的,不然在调用者中是无法使用的。我们接口的名字为onClickCallBackListener, 在其中有一个方法,该方法是接口回调时要执行的方法。
1 /* 2 *创建回调接口 3 */ 4 public static interface OnClickCallBackListener { 5 public void OnClickButton(View v); 6 }
(2) 声明一个私有的接口对象,并为这个私有的对象实现setter方法,该私有的接口对象是用来接受自定义组件调用者传过来的回调方法的。代码比较简单,在此就不做过多赘述了。
1 private OnClickCallBackListener callBackListener; //声明接口对象 2 public void setCallBackListener(OnClickCallBackListener callBackListener) { 3 this.callBackListener = callBackListener; 4 }
(3) 实现好接口以及接收回调对象的变量后,接下来要做的事情就是获取自定义组件中相应按钮点击的事件,并在此按钮点击事件中执行传过来的接口对象相应的回调方法。下方这个方法,要在构造函数中调用。该方法的功能就是获取自定义组件的相应按钮的点击事件并执行接口对象的回调方法。具体实现如下:
/* *点击按钮时执行接口回调 */ private void callBackButton() { Button button = (Button) findViewById(R.id.call_back); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (callBackListener != null) { callBackListener.OnClickButton(v); } } }); }
上方是把代码拆开来讲的,下方是整个自定义组件的实现类。具体代码如下所示。
1 package com.example.lizelu.customnavigationbar; 2 3 import android.annotation.TargetApi; 4 import android.app.Activity; 5 import android.content.Context; 6 import android.os.Build; 7 import android.util.AttributeSet; 8 import android.view.LayoutInflater; 9 import android.view.View; 10 import android.widget.Button; 11 import android.widget.FrameLayout; 12 import android.widget.TextView; 13 14 /** 15 * Created by lizelu on 15/11/29. 16 */ 17 public class CustomNavigationBar extends FrameLayout { 18 19 /* 20 *创建回调接口 21 */ 22 public static interface OnClickCallBackListener { 23 public void OnClickButton(View v); 24 } 25 26 27 private OnClickCallBackListener callBackListener; //声明接口对象 28 29 public String navigationTitle = "标题栏"; 30 31 /* 32 * 设置标题栏的标题 33 */ 34 public void setNavigationTitle(String navigationTitle) { 35 this.navigationTitle = navigationTitle; 36 TextView textView = (TextView) findViewById(R.id.navigation_title); 37 textView.setText(navigationTitle); 38 } 39 40 public void setCallBackListener(OnClickCallBackListener callBackListener) { 41 this.callBackListener = callBackListener; 42 } 43 44 /* 45 *自定义组件的构造方法 46 */ 47 public CustomNavigationBar(Context context, AttributeSet attrs) { 48 super(context, attrs); 49 LayoutInflater.from(context).inflate(R.layout.custom_navigation, this); //加载布局文件 50 onClickBackButton(); 51 callBackButton(); 52 } 53 /* 54 * 点击返回按钮方法 55 */ 56 private void onClickBackButton() { 57 Button button = (Button) findViewById(R.id.back_button); 58 button.setOnClickListener(new OnClickListener() { 59 @Override 60 public void onClick(View v) { 61 ((Activity) getContext()).finish(); 62 } 63 }); 64 } 65 66 /* 67 *点击按钮时执行接口回调 68 */ 69 private void callBackButton() { 70 Button button = (Button) findViewById(R.id.call_back); 71 button.setOnClickListener(new OnClickListener() { 72 @Override 73 public void onClick(View v) { 74 if (callBackListener != null) { 75 callBackListener.OnClickButton(v); 76 } 77 } 78 }); 79 } 80 }
三.该自定义组件的调用方式
经过上面的过程,我们自定控件以及实现好了,接下来就是如何使用了。其实自定义组件的使用方式和系统自带的组件使用起来区别不大,没有什么特别之处。下方就让我们在Activity中使用上述我们自定义的控件吧。
1.首先在我们要使用该组件的Activity所对应的布局文件中加载我们的自定义组件的布局。要注意的一点是自定义组件的标签我们要使用包的全面才可以,其他的和Android的系统组件使用方法类似,具体代码如下:
1 <com.example.lizelu.customnavigationbar.CustomNavigationBar 2 android:id="@+id/custom_navigation_bar" 3 android:layout_width="match_parent" 4 android:layout_height="wrap_content"/>
2.在Activity的Java类中,通过id获取我们自定义组件的对象,并实现其相应的回调即可。具体代码如下:
1 private void setNavigationTitle(String title) { 2 CustomNavigationBar navigationBar = (CustomNavigationBar) findViewById(R.id.custom_navigation_bar); 3 navigationBar.setNavigationTitle(title); 4 5 //实现组件上的按钮的接口回调 6 navigationBar.setCallBackListener(new CustomNavigationBar.OnClickCallBackListener() { 7 @Override 8 public void OnClickButton(View v) { 9 Toast.makeText(MainActivity.this, "回调执行的方法", Toast.LENGTH_SHORT).show(); 10 } 11 }); 12 }
到此,自定义组件的实现和调用实现完毕。虽然上述自定义控件虽然比较简单,但是麻雀虽小,五脏俱全。再复杂的自定义控件也是有简单的东西慢慢的拼装而成。所以理解自定义控件的实现原理还是比较重要的。今天的博客就先到这儿,下方是上述Demo在GitHub上的分享地址,需要的小伙伴请自行Clone。
github分享地址:https://github.com/lizelu/AndroidCustomNavigationBar