Custom_Android_UI
安卓自定义菜单子项控件,底部弹出框控件,验证码输入框控件。
没有做成插件,因为美观没有进行处理,不过功能是出来了,想用的码友可以把源码拿去稍微美化下使用。
这里一共做了三个自定义UI:
- 菜单子列项
- 底部弹出框
- 验证码输入框
直接先来看看实现图把:
菜单子列项
底部弹出框
验证码输入框
好的,不是很美观,这里我们主要说说功能的实现讨论。
菜单子列项
这个实现起来比较简单,定义一个布局,这里我们继承FrameLayout类进行定制我们自定义控件,我们不需要重写布局方法和绘制方法,这里我们主要做个功能的集成就好,比如点击事件的回调,文本内容的获取设置以及图片的设置等。
设置属性
//这里我们设置了三个属性
//title属性 文本标题
//hint属性 提示文字
//icon属性 icon图标
<com.example.a86157.custom_ui.Menu.Menu_Child_Layout
android:layout_width="match_parent"
android:layout_height="50dp"
app:title="WX"
app:hint="马化腾"
app:icon="@drawable/ahms"/>
那么我们怎么进行这种属性的绑定操作呢,很简单,我们定义个xml文件,使用 declare-styleable 来定义我们自定义控件的属性,那么你又可能会想问为啥我要用declare-styleable这个东西来定义,这样说吧,我们安卓基础控件TextView有个text属性,其实在源码中也是有这样的类似xml文件定义
看谷歌爸爸的源码也是通过R.Sty…啥的读取,那么这里我们也是这样定义我们自己的控件的属性咯。
在代码中就这样进行读取我们的配置属性信息:
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Menu_Child_Layout);
setTitle(a.getString(R.styleable.Menu_Child_Layout_title));
setHint(a.getString(R.styleable.Menu_Child_Layout_hint));
setImage(a.getResourceId(R.styleable.Menu_Child_Layout_icon, 10000));
点击事件回调
public void setViewOnlickListener(OnClickListener onlickListener){
view.setOnClickListener(onlickListener);
}
设置图片
setImage(a.getResourceId(R.styleable.Menu_Child_Layout_icon, 10000));
嗯,那么这个最简单的控件我们就介绍完毕了,下面我们来说一下 底部弹出框 实现,(验证码输入框也是依葫芦画瓢就不讲这个的实现了,有兴趣的看下源码就懂了)
底部弹出框:
这里我们定义一个类继承了ViewGroup重写布局以及测量方法,这里唯一的难点就是该使用怎样的布局以及高度的测量,宽度就是百分之百了。
这里需要科普一下ViewGroup以及View这俩个类嘛?
我还是科普一下,凑凑字数把,等下文章内容太少显得我写的文章太low了。
View就是所有UI控件的老祖宗,虽然我们View也使用了ViewGroup包裹,但是我们的ViewGrop是集成了View写的。
看下ViewGroup的源码,没错把,确实继承了View,
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
private static final String TAG = "ViewGroup";
@UnsupportedAppUsage
private static final boolean DBG = false;
/**
* Views which have been hidden or removed which need to be animated on
* their way out.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
@UnsupportedAppUsage
protected ArrayList<View> mDisappearingChildren;
........
再来看看我们很熟悉了TextView:
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
static final String LOG_TAG = "TextView";
static final boolean DEBUG_EXTRACT = false;
private static final float[] TEMP_POSITION = new float[2];
// Enum for the "typeface" XML parameter.
// TODO: How can we get this from the XML instead of hardcoding it here?
/** @hide */
@IntDef(value = {DEFAULT_TYPEFACE, SANS, SERIF, MONOSPACE})
@Retention(RetentionPolicy.SOURCE)
public @interface XMLTypefaceAttr{}
private static final int DEFAULT_TYPEFACE = -1;
private static final int SANS = 1;
private static final int SERIF = 2;
private static final int MONOSPACE = 3;
// Enum for the "ellipsize" XML parameter.
........
没错把,都是继承了View写的。
那么我们平时使用界面的关系应该是这样的:
window -> viewgroup -> view
一个window一个viewgroup多个view
就是这样的关系啦。
View里面有我们组件常用的属性,比如内边距外边距,监听等一些组件的公共属性。
比如Backgournd:
@ViewDebug.ExportedProperty(deepExport = true, prefix = "bg_")
@UnsupportedAppUsage
private Drawable mBackground;
private TintInfo mBackgroundTint;
........
比如内边距属性:
@ViewDebug.ExportedProperty(category = "padding")
@UnsupportedAppUsage
protected int mPaddingLeft = 0;
/**
* The final computed right padding in pixels that is used for drawing. This is the distance in
* pixels between the right edge of this view and the right edge of its content.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "padding")
@UnsupportedAppUsage
protected int mPaddingRight = 0;
/**
* The final computed top padding in pixels that is used for drawing. This is the distance in
* pixels between the top edge of this view and the top edge of its content.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "padding")
@UnsupportedAppUsage
protected int mPaddingTop;
........
看了下源码,这样子对View是不是有点小了解了。
那么ViewGroup就是包含了很多个View,是不是有点想我们的布局,比如FrameLayout,线性布局,相对布局等等。都是继承ViewGroup进行重写。
上源码看:
* only if {@link #setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring()}
* is set to true.
*
* @attr ref android.R.styleable#FrameLayout_measureAllChildren
*/
@RemoteView
public class FrameLayout extends ViewGroup {
private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;
.......
ViewGroup实现了四种构造方法:
public ViewGroup(Context context) {
this(context, null);
}
public ViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initViewGroup();
initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
}
这是必须实现的方法,用来定位我们组件的位置
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
//ViewGroup中包含组件的宽高设置
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
同时我们最好在重写一下View里面测量组件宽高的方法,竟然自定义了当然这么重要的也要重写
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
这里我们就可以进行设置了组件的
1.颜色
2.文本
3.图片
4.定位
5.宽高
这样子我们就能定制大部分自定义组件的需求了。
这里我们就简单的科普了一下View和ViewGroup。言归正传,来说一下我们底部弹出框的实现逻辑:
首先我们定义了一个bottom_layout.xml的布局文件
<FrameLayout
android:id="@+id/layout1"
android:layout_gravity="bottom"
android:layout_margin="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text1"
android:layout_width="100dp"
android:layout_height="10dp"
android:layout_gravity="center"
android:layout_margin="2dp"
android:background="@drawable/shape" />
</FrameLayout>
很简单就用了一个Framlayout布局里面放置一个TextView设置置底部完毕。 然后我们把这个布局添加到我们自定义控件: 这是我们的自定义控件类:
``` public class Bottom_Layout extends ViewGroup{ private View view; private Context context; private TextView toptv; private FrameLayout frameLayout1; private boolean bool=false; private int value; private KeyBoardHelper boardHelper;
public Bottom_Layout(Context context) {
super(context);
}
public Bottom_Layout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public Bottom_Layout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context=context;
init(context,attrs);
}
private void init(Context context, AttributeSet attrs){
//LayoutInflater 取得xml里定义的view
LayoutInflater inflater=(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.bottom_layout,this,true);
toptv = view.findViewById(R.id.text1);
frameLayout1 = view.findViewById(R.id.layout1);
toptv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(bool){
bool=!bool;
close_state();
}else{
bool=!bool;
open_state(0);
}
}
});
Activity activity = (Activity) context;
boardHelper = new KeyBoardHelper(activity);
boardHelper.onCreate();
boardHelper.setOnKeyBoardStatusChangeListener(onKeyBoardStatusChangeListener);
}
private KeyBoardHelper.OnKeyBoardStatusChangeListener onKeyBoardStatusChangeListener = new KeyBoardHelper.OnKeyBoardStatusChangeListener() {
@Override
public void OnKeyBoardPop(int keyBoardheight) {
open_state(keyBoardheight);
}
@Override
public void OnKeyBoardClose(int oldKeyBoardheight) {
open_state(0);
}
};
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
int height = wm.getDefaultDisplay().getHeight();
value = height-Utils.getStatusBarHeight(context)-frameLayout1.getMeasuredHeight()-Utils.getNavigationBarHeight(context);
close_state();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
private void open_state(int subjoin){
for(int i=0;i<getChildCount();i++){
View childview = getChildAt(i);
if(i==0){
childview.layout(0,0,getMeasuredWidth(),value-getChildAt(1).getMeasuredHeight()-subjoin);
} else{
childview.layout(0,value-childview.getMeasuredHeight()-subjoin,getMeasuredWidth(),value-subjoin);
}
}
}
private void close_state(){
for(int i=0;i<getChildCount();i++){
View childview = getChildAt(i);
if(i==0){
childview.layout(0,0,getMeasuredWidth(),value);
} else{
childview.layout(0,value,getMeasuredWidth(),value+getMeasuredHeight());
}
}
}
}
这里我们是怎么把bottom_layout.xml布局添加到我们自定义控件的呢,如果你看过LayoutInflater这块的源码就不用我多说了,如果你不了解我现在给你总结下:
*inflater.inflate(R.layout.bottom_layout,this,true);*
> 如果root为null或者attachToRoot为false时,则调用layout.xml中的根布局的属性并且将其作为一个View对象返回。
> 如果root不为null,但attachToRoot为false时,则先将layout.xml中的根布局转换为一个View对象,再调用传进来的root的布局属性设置给这个View,然后将它返回。
> 如果root不为null,且attachToRoot为true时,则先将layout.xml中的根布局转换为一个View对象,再将它add给root,最终再把root返回出去。(两个参数的inflate如果root不为null也是相当于这种情况)
[Android LayoutInflater.inflate各个参数作用](https://www.jianshu.com/p/3f871d95489c)
那么我们就把一个小横线固定的放进来了我们的自定义控件里面,由于我们用的是ViewGroup这就相当于一个Framlayout帧布局,随意摆放,随意叠加。我们把小横线的宽高设置为默认,定位我们设置为 childview.layout(0,0,getMeasuredWidth(),value); 全屏这样小横线默认就在底部。
后面添加进来的组件我们让它出现在屏幕下面,就实现了不可见状态,如果需要可见,则我们把组件整体向上移动:
private void open_state(int subjoin){
for(int i=0;i<getChildCount();i++){
View childview = getChildAt(i);
if(i==0){
childview.layout(0,0,getMeasuredWidth(),value-getChildAt(1).getMeasuredHeight()-subjoin);
} else{
childview.layout(0,value-childview.getMeasuredHeight()-subjoin,getMeasuredWidth(),value-subjoin);
}
}
}
这里需要传进来一个参数,subjoin 这个参数是为了当软键盘出来的时候我们把整个弹窗口往上挤。默认就设置为0,然后监听键盘的弹出以及关闭设置subjoin大小。subjoin大小就设置为键盘的高度:
//这里是一个封装回调
//封成了一个监听类,在Util中可以直接去调用
private KeyBoardHelper.OnKeyBoardStatusChangeListener onKeyBoardStatusChangeListener = new KeyBoardHelper.OnKeyBoardStatusChangeListener() {
@Override
public void OnKeyBoardPop(int keyBoardheight) {
open_state(keyBoardheight);
}
@Override
public void OnKeyBoardClose(int oldKeyBoardheight) {
open_state(0);
}
};
这里我们的底部弹出框就简单介绍完毕了。
*后面有空我们再来仔细说受View以及ViewGroup的实现和View的分发机制原理啊。*