安卓 (菜单子列项,底部弹出框,验证码输入框) 自定义控件实现

Custom_Android_UI

项目Github地址,点击前往。

安卓自定义菜单子项控件,底部弹出框控件,验证码输入框控件。

没有做成插件,因为美观没有进行处理,不过功能是出来了,想用的码友可以把源码拿去稍微美化下使用。

这里一共做了三个自定义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…啥的读取,那么这里我们也是这样定义我们自己的控件的属性咯。

declare-styleable属性介绍

在代码中就这样进行读取我们的配置属性信息:

  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的分发机制原理啊。*


发布了20 篇原创文章 · 获赞 3 · 访问量 436

猜你喜欢

转载自blog.csdn.net/weixin_41078100/article/details/102949668
今日推荐