layout_layoutinflater - CSDN
精华内容
参与话题
  • Layout(布局)

    千次阅读 2018-10-12 22:44:38
    布局容器有5个区域:北、南、东、西和中间。中间区域面板是必须的,边缘的面板都是可选的。...布局可以进行嵌套,用户可以通过组合布局构建复杂的布局结构   <%@ page language="...utf-...

                    布局容器有5个区域:北、南、东、西和中间。中间区域面板是必须的,边缘的面板都是可选的。每个边缘区域面板都可以通过拖拽其边框改变大小,也可以点击折叠按钮将面板折叠起来。布局可以进行嵌套,用户可以通过组合布局构建复杂的布局结构

     

    <%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
      <head>
        <title>Layout:通过标签创建布局</title>
        <!-- 引入easyUI:JQuery文件支持 -->
      	<script type="text/javascript" src="js/jquery-easyui-1.3.4/jquery.min.js"></script>
      	<!-- 引入easyUI -->
      	<script type="text/javascript" src="js/jquery-easyui-1.3.4/jquery.easyui.min.js"></script>
      	<!-- 引入中文支持 -->
      	<script type="text/javascript" src="js/jquery-easyui-1.3.4/locale/easyui-lang-zh_CN.js"></script>
      	<!-- 引入easyUI主题css文件 -->
      	<link rel="stylesheet" href="js/jquery-easyui-1.3.4/themes/default/easyui.css" type="text/css"></link>
      	<!-- 引入easyUI 图标导航文件 -->
      	<link rel="stylesheet" href="js/jquery-easyui-1.3.4/themes/icon.css" type="text/css"></link></head>
      
      <body>
      	<%
    		/*
    			为div标签增加名为'easyui-layout'的类ID
    			region:布局面板位置;  title:布局面板标题;  split:true时可以通过分割栏改变面板大小
    			iconCls:包含图标的CSS类ID(icon-**)
    			可以在标签中间层输入html或文本
    		*/  	
      	 %>
    	<div id="cc" class="easyui-layout" style="width:600px;height:400px;">   
    		<div data-options="region:'north',title:'上',split:false" style="height:100px;">Up</div>   
    	    <div data-options="region:'south',title:'下',split:true" style="height:100px;">Down</div>   
    	    <div data-options="region:'east',iconCls:'icon-no',title:'东',split:true" style="width:100px;"></div>   
    	    <div data-options="region:'west',title:'西',split:true" style="width:100px;"></div>   
    	    <div data-options="region:'center',title:'中'" style="padding:5px;background:#eee;"><h1>墨渐生微</h1></div>   
    	</div>
      </body>
    </html>
    

     

    <%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
      <head>
        <title>Layout:使用完整页面创建布局</title>
        <!-- 引入easyUI:JQuery文件支持 -->
      	<script type="text/javascript" src="js/jquery-easyui-1.3.4/jquery.min.js"></script>
      	<!-- 引入easyUI -->
      	<script type="text/javascript" src="js/jquery-easyui-1.3.4/jquery.easyui.min.js"></script>
      	<!-- 引入中文支持 -->
      	<script type="text/javascript" src="js/jquery-easyui-1.3.4/locale/easyui-lang-zh_CN.js"></script>
      	<!-- 引入easyUI主题css文件 -->
      	<link rel="stylesheet" href="js/jquery-easyui-1.3.4/themes/default/easyui.css" type="text/css"></link>
      	<!-- 引入easyUI 图标导航文件 -->
      	<link rel="stylesheet" href="js/jquery-easyui-1.3.4/themes/icon.css" type="text/css"></link></head>
      
    	<body class="easyui-layout">
    		 <%
    			/*
    				为div标签增加名为'easyui-layout'的类ID
    				region:布局面板位置;  title:布局面板标题;  split:true时可以通过分割栏改变面板大小
    				iconCls:包含图标的CSS类ID(icon-**)
    			*/  	
    	  	 %>	  
    	    <div data-options="region:'north',title:'North Title',split:true" style="height:100px;"></div>   
    	    <div data-options="region:'south',title:'South Title',split:true" style="height:100px;"></div>   
    	    <div data-options="region:'east',iconCls:'icon-reload',title:'East',split:true" style="width:100px;"></div>   
    	    <div data-options="region:'west',title:'West',split:true" style="width:100px;"></div>   
    	    <div data-options="region:'center',title:'center title'" style="padding:5px;background:#eee;"></div>   
    	</body> 
    </html>
    

     

    <%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
    	<head>
    	    <title>Layout:嵌套布局</title>
    	    <!-- 引入easyUI:JQuery文件支持 -->
    	  	<script type="text/javascript" src="js/jquery-easyui-1.3.4/jquery.min.js"></script>
    	  	<!-- 引入easyUI -->
    	  	<script type="text/javascript" src="js/jquery-easyui-1.3.4/jquery.easyui.min.js"></script>
    	  	<!-- 引入中文支持 -->
    	  	<script type="text/javascript" src="js/jquery-easyui-1.3.4/locale/easyui-lang-zh_CN.js"></script>
    	  	<!-- 引入easyUI主题css文件 -->
    	  	<link rel="stylesheet" href="js/jquery-easyui-1.3.4/themes/default/easyui.css" type="text/css"></link>
    	  	<!-- 引入easyUI 图标导航文件 -->
    	  	<link rel="stylesheet" href="js/jquery-easyui-1.3.4/themes/icon.css" type="text/css"></link>
    	</head>
      
    	<body class="easyui-layout">
    		<%
    			/*
    				fit:布局组件将自适应父容器
    				collapsed:是否显示折叠按钮
    			*/  	
    	  	 %>   
    	    <div data-options="region:'north'" style="height:100px"></div>   
    	    <div data-options="region:'center'">   
    	        <div class="easyui-layout" data-options="fit:true">   
    	            <div data-options="region:'west',collapsed:true" style="width:180px"></div>   
    	            <div data-options="region:'center'"></div>   
    	        </div>   
    	    </div>   
    	</body> 
    </html>
    

     

    <%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
    	<head>
    	    <title>JS代码创建布局</title>
    	    <!-- 引入easyUI:JQuery文件支持 -->
    	  	<script type="text/javascript" src="js/jquery-easyui-1.3.4/jquery.min.js"></script>
    	  	<!-- 引入easyUI -->
    	  	<script type="text/javascript" src="js/jquery-easyui-1.3.4/jquery.easyui.min.js"></script>
    	  	<!-- 引入中文支持 -->
    	  	<script type="text/javascript" src="js/jquery-easyui-1.3.4/locale/easyui-lang-zh_CN.js"></script>
    	  	<!-- 引入easyUI主题css文件 -->
    	  	<link rel="stylesheet" href="js/jquery-easyui-1.3.4/themes/default/easyui.css" type="text/css"></link>
    	  	<!-- 引入easyUI 图标导航文件 -->
    	  	<link rel="stylesheet" href="js/jquery-easyui-1.3.4/themes/icon.css" type="text/css"></link>
    	  	
    	  	<script type="text/javascript">
    	  		$(document).ready(function(){
    		  		$('#cc').layout('add',{    
    				    region: 'west',    
    				    width: 180,    
    				    title: '西',    
    				    split: true,    
    				    tools: [{    
    					        iconCls:'icon-add',    
    					        handler:function(){alert('add');}    
    					    },{    
    					        iconCls:'icon-remove',    
    					        handler:function(){alert('remove');}    
    					    }]
    				});
    	  		});  		
    	  	</script>
    	</head>
      
    	<body>
    		<div id="cc" class="easyui-layout" style="width:600px;height:400px;"> 
    			<div data-options="region:'center',title:'center title'" style="padding:5px;background:#eee;"></div> 
    		</div>
    	</body> 
    </html>
    

     

    展开全文
  • 关于Layout的总结。

    千次阅读 2018-07-14 08:42:05
    1.关于coordinatorLayoutCoordinatorLayout简介CoordinatorLayout是在 Google IO/15 大会发布的,遵循Material 风格,包含在 support Library中,结合AppbarLayout, CollapsingToolbarLayout等 可 产生各种炫酷的...

    CoordinatorLayout简介

    CoordinatorLayout是在 Google IO/15 大会发布的,遵循Material 风格,包含在 support Library中,结合AppbarLayout, CollapsingToolbarLayout等 可 产生各种炫酷的效果


    简单来说就是

    • 作为最上层的View
    • 作为一个 容器与一个或者多个子View进行交互
    与AppbarLayout组合的滚动布局(RecyclerView, NestedScrollView等),需要设置 app:layout_behavior = "@string/appbar_scrolling_view_behavior" .没有设置的话, AppbarLayout将不会响应滚动布局的滚动事件.

    类型说明
    int SCROLL_FLAG_ENTER_ALWAYSWhen entering (scrolling on screen) the view will scroll on any downwards scroll event, regardless of whether the scrolling view is also scrolling.
    int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSEDAn additional flag for 'enterAlways' which modifies the returning view to only initially scroll back to it's collapsed height.
    int SCROLL_FLAG_EXIT_UNTIL_COLLAPSEDWhen exiting (scrolling off screen) the view will be scrolled until it is 'collapsed'.
    int SCROLL_FLAG_SCROLLThe view will be scroll in direct relation to scroll events.
    int SCROLL_FLAG_SNAPUpon a scroll ending, if the view is only partially visible then it will be snapped and scrolled to it's closest edge.
    类型说明
    int SCROLL_FLAG_ENTER_ALWAYSW((entering) / (scrolling on screen))下拉的时候,这个View也会跟着滑出。
    int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED另一种enterAlways,但是只显示折叠后的高度。
    int SCROLL_FLAG_EXIT_UNTIL_COLLAPSED((exiting) / (scrolling off screen))上拉的时候,这个View会跟着滑动直到折叠。
    int SCROLL_FLAG_SCROLL这个View将会响应Scroll事件
    int SCROLL_FLAG_SNAP在Scroll滑动事件结束以前 ,如果这个View部分可见,那么这个View会停在最接近当前View的位置

    我们可以通过两种 方法设置这个Flag

    • 方法一
     setScrollFlags(int) 
    
    • 方法二
     app:layout_scrollFlags="scroll|enterAlways"
    

    注意事项

    AppBarLayout必须作为CoordinatorLayout的直接子View,否则它的大部分功能将不会生效,如layout_scrollFlags等。


    CollapsingToolbarLayout

    CollapsingToolbarLayout继承与FrameLayout,官网地址,请自备梯子。

    简单来说 ,CollapsingToolbarLayout是工具栏的包装器,它通常作为AppBarLayout的孩子。主要实现以下功能

    • Collapsing title(可以折叠 的 标题 )
    • Content scrim(内容装饰),当我们滑动的位置 到达一定阀值的时候,内容 装饰将会被显示或者隐藏
    • Status bar scrim(状态栏布)
    • Parallax scrolling children,滑动的时候孩子呈现视觉特差效果
    • Pinned position children,固定位置的 孩子

    下面我们一起来看一下几个常量

    常量解释说明
    int COLLAPSE_MODE_OFFThe view will act as normal with no collapsing behavior.(这个 View将会 呈现正常的结果,不会表现出折叠效果)
    int COLLAPSE_MODE_PARALLAXThe view will scroll in a parallax fashion. See setParallaxMultiplier(float) to change the multiplier used.(在滑动的时候这个View 会呈现 出 视觉特差效果 )
    int COLLAPSE_MODE_PINThe view will pin in place until it reaches the bottom of the CollapsingToolbarLayout.(当这个View到达 CollapsingToolbarLayout的底部的时候,这个View 将会被放置,即代替整个CollapsingToolbarLayout)

    我们有两种方法可以设置这个常量,

    方法一:在代码中使用这个方法

    setCollapseMode(int collapseMode)
    

    方法 二:在布局文件中使用自定义属性

    app:layout_collapseMode="pin"
    

    DrawerLayout 简单使用

    介绍

    drawerLayout是Support Library包中实现了侧滑菜单效果的控件,可以说drawerLayout是因为第三方控件如MenuDrawer等的出现之后,google借鉴而出现的产物。drawerLayout分为侧边菜单和主内容区两部分,侧边菜单可以根据手势展开与隐藏(drawerLayout自身特性),主内容区的内容可以随着菜单的点击而变化(这需要使用者自己实现)。

    实用

    DrawerLayout 实用比较简单,这里只介绍简单使用和常用API。

    1. 在布局文件中使用DrawerLayout 进行设置。如下示例代码:
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/dlyt_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    
        <!-- 内容 -->
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <Button
                android:id="@+id/btn_context"
                android:text="context"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
    
        </FrameLayout>
    
        <!-- 左边菜单 -->
        <FrameLayout
            android:background="@color/colorAccent"
            android:layout_gravity="start"
            android:layout_width="200dp"
            android:layout_height="match_parent">
            <Button
                android:id="@+id/btn_left_menu"
                android:text="left menu"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </FrameLayout>
    
        <!-- 右边菜单 -->
        <FrameLayout
            android:background="@color/colorAccent"
            android:layout_gravity="end"
            android:layout_width="200dp"
            android:layout_height="match_parent">
            <Button
                android:id="@+id/btn_right_menu"
                android:text="right menu"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </FrameLayout>
    </android.support.v4.widget.DrawerLayout>
    
    

    在 DrawerLayout 布局中有三个元素,

    1. view 主体
    2. 左边菜单:
      左边菜单的设定使用android:layout_gravity="start" 设置。
    3. 右边菜单:
      右边菜单的设定使用android:layout_gravity="end" 设置。

    通过上面,我们就已经实现了左右划出菜单效果。
    如果我们需要加上一些控制逻辑,可能就需要一些的一些API了。

    DrawerLayout 中部分 API 介绍

    1. isDrawerOpen(@EdgeGravity int drawerGravity) : 判断菜单是否打开。

    传入参数:

    • GravityCompat.START : 左边菜单是否打开。
    • GravityCompat.END : 右边菜单是否打开。

    返回值

    • 打开 : true
    • 关闭 : false
    1. openDrawer(@EdgeGravity int gravity) : 打开菜单

    传入参数:

    • GravityCompat.START : 打开左边菜单。
    • GravityCompat.END : 打开右边菜单。
    1. closeDrawer(@EdgeGravity int gravity) : 关闭菜单

    传入参数:

    • GravityCompat.START : 关闭左边菜单。
    • GravityCompat.END : 关闭右边菜单。
    1. addDrawerListener(@NonNull DrawerListener listener) : 添加监听
    2. DrawerListener 类
    • onDrawerSlide(View drawerView, float slideOffset) : 滑动时调用
    • onDrawerOpened(View drawerView) : 打开菜单时调用
    • onDrawerClosed(View drawerView) : 关闭菜单时调用
    • onDrawerStateChanged(@State int newState) : 菜单状态改变时调用

    其他API查看源码就会明白如何使用。

    如下Java示例代码:

    //添加监听
    DrawerLayout.addDrawerListener(new DrawerViewListener());
    
    private class DrawerViewListener implements DrawerLayout.DrawerListener {
        @Override
        public void onDrawerSlide(View drawerView, float slideOffset) {
        //滑动时调用
        }
    
        @Override
        public void onDrawerOpened(View drawerView) {
        //打开菜单时调用
        }
    
        @Override
        public void onDrawerClosed(View drawerView) {
        //关闭菜单时调用
        }
    
        @Override
        public void onDrawerStateChanged(int newState) {
        //菜单状态改变时调用
        }
    }
    

    AutoCompleteTextView

    AutoCompleteTextView常用属性

    属性描述
    android:completionHint设置出现在下拉菜单底部的提示信息
    android:completionThreshold设置触发补全提示信息的字符个数
    android:dropDownHorizontalOffset设置下拉菜单于文本框之间的水平偏移量
    android:dropDownHeight设置下拉菜单的高度
    android:dropDownWidth设置下拉菜单的宽度
    android:singleLine设置单行显示文本内容
    android:dropDownVerticalOffset设置下拉菜单于文本框之间的垂直偏移量

    使用ArrayAdapter来作为AutoCompleteTextView的数据适配器

    • 简单的xml布局
    <AutoCompleteTextView
        android:id="@+id/tv_search"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/hint_type"
        android:completionHint="@string/chint_recent"
        android:completionThreshold="1" />
    
    • 默认AutoCompleteTextView中的数据保存在SharedPreferences中,故将SharedPreferences做了简单的API封装以方便数据存取,详细的SharedPreferences请参考这里:SharedPreferences
    // 从SharedPreferences中获取历史记录数据
    private String getHistoryFromSharedPreferences(String key) {
        SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_PRIVATE);
        return sp.getString(key, SP_EMPTY_TAG);
    }
    
    // 将历史记录数据保存到SharedPreferences中
    private void saveHistoryToSharedPreferences(String key, String history) {
        SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.putString(key, history);
        editor.apply();
    }
    
    // 清除保存在SharedPreferences中的历史记录数据
    private void clearHistoryInSharedPreferences() {
        SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.clear();
        editor.apply();
    }
    
    • 使用默认适配器的AutoCompleteTextView相关初始化
    private void initSearchView() {
        mSearchTv = (AutoCompleteTextView) findViewById(R.id.tv_search);
        String[] mSearchHistoryArray = getHistoryArray(SP_KEY_SEARCH);
        mSearchAdapter = new ArrayAdapter<>(
                this,
                android.R.layout.simple_dropdown_item_1line,
                mSearchHistoryArray
        );
        mSearchTv.setAdapter(mSearchAdapter);  // 设置适配器
    
        // 设置下拉提示框的高度为200dp
        // mAutoCompleteTv.setDropDownHeight();      // 或XML中为android:dropDownHeight="200dp"
    
        // 默认当输入2个字符以上才会提示, 现在当设置输入1个字符就自动提示
        // mAutoCompleteTv.setThreshold(1);          // 或XML中为android:completionThreshold="1"
    
        // 设置下拉提示框中底部的提示
        // mAutoCompleteTv.setCompletionHint("最近的5条记录");
    
        // 设置单行输入限制
        // mAutoCompleteTv.setSingleLine(true);
    }
    
    private String[] getHistoryArray(String key) {
        String[] array = getHistoryFromSharedPreferences(key).split(SP_SEPARATOR);
        if (array.length > MAX_HISTORY_COUNT) {         // 最多只提示最近的50条历史记录
            String[] newArray = new String[MAX_HISTORY_COUNT];
            System.arraycopy(array, 0, newArray, 0, MAX_HISTORY_COUNT); // 实现数组间的内容复制
        }
        return array;
    }
    
    • 保存AutoCompleteTextView中的历史记录数据到SharedPreferences中
    private void saveSearchHistory() {
        String text = mSearchTv.getText().toString().trim();       // 获取搜索框文本信息
        if (TextUtils.isEmpty(text)) {                      // null or ""
            Toast.makeText(this, "Please type something again.", Toast.LENGTH_SHORT).show();
            return;
        }
    
        String old_text = getHistoryFromSharedPreferences(SP_KEY_SEARCH);// 获取SP中保存的历史记录
        StringBuilder sb;
        if (SP_EMPTY_TAG.equals(old_text)) {
            sb = new StringBuilder();
        } else {
            sb = new StringBuilder(old_text);
        }
        sb.append(text + SP_SEPARATOR);      // 使用逗号来分隔每条历史记录
    
        // 判断搜索内容是否已存在于历史文件中,已存在则不再添加
        if (!old_text.contains(text + SP_SEPARATOR)) {
            saveHistoryToSharedPreferences(SP_KEY_SEARCH, sb.toString());  // 实时保存历史记录
            mSearchAdapter.add(text);        // 实时更新下拉提示框中的历史记录
            Toast.makeText(this, "Search saved: " + text, Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "Search existed: " + text, Toast.LENGTH_SHORT).show();
        }
    }
    

    上面代码中,为了能够实时更新下拉提示框中的历史记录,需要在保存数据后再调用ArrayAdapter.add()方法,而不是调用ArrayAdapter.notifyDataSetChanged()

    • 实时清除下拉提示框中的历史记录
    clearHistoryInSharedPreferences();          // 试试清除历史记录
    mSearchAdapter.clear();                     // 实时清除下拉提示框中的历史记录
    
    • 效果演示
    arrayadapter_autocomplete_textview_320x512.gif

    使用自定义AutoCompleteAdapter来作为AutoCompleteTextView的数据适配器

    • 简单的xml布局

    使用RelativeLayout来容纳AutoCompleteTextView和ImageView,其中ImageView位于右侧,用于点击清除AutoCompleteTextView的内容

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:gravity="center_vertical">
    
        <AutoCompleteTextView
            android:id="@+id/tv_custom"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingStart="12dp"
            android:paddingEnd="40dp"
            android:hint="@string/hint_type"/>
    
        <ImageView
            android:id="@+id/iv_custom"
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:layout_marginEnd="10dp"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true"
            android:scaleType="fitCenter"
            android:src="@drawable/ic_action_name"
            android:contentDescription="@null"/>
    </RelativeLayout>
    
    • 使用自定义适配器的AutoCompleteTextView相关初始化
    private void initCustomView() {
        mCustomTv = (AutoCompleteTextView) findViewById(R.id.tv_custom);
        mDeleteIv = (ImageView) findViewById(R.id.iv_custom);
        mDeleteIv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mCustomTv.setText("");              // 清空TextView的内容
            }
        });
    
        ArrayList<String> mOriginalValues = new ArrayList<>();
        String[] mCustomHistoryArray = getHistoryArray(SP_KEY_CUSTOM);
        mOriginalValues.addAll(Arrays.asList(mCustomHistoryArray));     // String[] => ArrayList<String>
        
        mCustomAdapter = new AutoCompleteAdapter(this, mOriginalValues);
        mCustomAdapter.setDefaultMode(AutoCompleteAdapter.MODE_STARTSWITH | AutoCompleteAdapter.MODE_SPLIT);// 设置匹配模式
        mCustomAdapter.setSupportPreview(true);     // 支持使用特殊符号进行预览提示内容,默认为'@'
    
        simpleItemHeight = mCustomAdapter.getSimpleItemHeight();
        Toast.makeText(this, "simpleItemHeight: " + simpleItemHeight, Toast.LENGTH_SHORT).show(); // 103
    
        mCustomAdapter.setOnFilterResultsListener(new AutoCompleteAdapter.OnFilterResultsListener() {
            @Override
            public void onFilterResultsListener(int count) {
                curCount = count;
                if (count > MAX_ONCE_MATCHED_ITEM) {        // 限制提示框最多要显示的记录行数
                    curCount = MAX_ONCE_MATCHED_ITEM;
                }
                if (curCount != prevCount) {                // 仅当目前的数目和之前的不同才重新设置下拉框高度,避免重复设置
                    prevCount = curCount;
                    mCustomTv.setDropDownHeight(simpleItemHeight * curCount);
                }
            }
        });
    
        mCustomAdapter.setOnSimpleItemDeletedListener(new AutoCompleteAdapter.OnSimpleItemDeletedListener() {
            @Override
            public void onSimpleItemDeletedListener(String value) {
                String old_history = getHistoryFromSharedPreferences(SP_KEY_CUSTOM);    // 获取之前的记录
                String new_history = old_history.replace(value + SP_SEPARATOR, "");    // 用空字符串替换掉要删除的记录
                saveHistoryToSharedPreferences(SP_KEY_CUSTOM, new_history);             // 保存修改过的记录
            }
        });
    
        mCustomTv.setAdapter(mCustomAdapter);       //
        mCustomTv.setThreshold(1);                  //
    
        // 设置下拉时显示的提示行数 (此处不设置也可以,因为在AutoCompleteAdapter中有专门的事件监听来实时设置提示框的高度)
        // mCustomTv.setDropDownHeight(simpleItemHeight * MAX_ONCE_MATCHED_ITEM);
    }
    
    • 保存AutoCompleteTextView中的历史记录数据到SharedPreferences中
    private void saveCustomHistory() {
        String text = mCustomTv.getText().toString().trim();     // 获取搜索框信息
        if (TextUtils.isEmpty(text)) {          // null or ""
            Toast.makeText(this, "Please type something again.", Toast.LENGTH_SHORT).show();
            return;
        }
    
        String old_text = getHistoryFromSharedPreferences(SP_KEY_CUSTOM);    // 获取SP中保存的历史记录
        StringBuilder sb;
        if (SP_EMPTY_TAG.equals(old_text)) {
            sb = new StringBuilder();
        } else {
            sb = new StringBuilder(old_text);
        }
        sb.append(text + SP_SEPARATOR);      // 使用逗号来分隔每条历史记录
    
        // 判断搜索内容是否已存在于历史文件中,已存在则不再添加
        if (!old_text.contains(text + SP_SEPARATOR)) {
            saveHistoryToSharedPreferences(SP_KEY_CUSTOM, sb.toString());  // 实时保存历史记录
            mCustomAdapter.add(text);        // 实时更新下拉提示框中的历史记录
            Toast.makeText(this, "Custom saved: " + text, Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "Custom existed: " + text, Toast.LENGTH_SHORT).show();
        }
    }
    
    • 实时清除下拉提示框中的历史记录
    clearHistoryInSharedPreferences();      // 试试清除历史记录
    mCustomAdapter.clear();                 // 实时清除下拉提示框中的历史记录
    
    • 自定义适配器AutoCompleteAdapter

    AutoCompleteAdapter参考了ArrayAdapter的部分源代码,继承自BaseAdapter并实现Filterable接口,实现了以下功能:

    1. 实现自动补全的匹配模式的配置,有三种可选匹配模式:
    MODE_CONTAINS / MODE_STARTSWITH(default) / MODE_SPLIT
    
    1. 实现匹配成功事件的回调,用于根据匹配结果数来动态设置下拉提示框的高度
    2. 实现删除匹配结果中子项的事件回调,用于实时更新存储在SharedPreferences的历史记录数据
    3. 支持使用@字符来预览所有提示内容
    public class AutoCompleteAdapter extends BaseAdapter implements Filterable {
    
        private static final int MODE_NONE = 0x000;                 // 0000b
        public static final int MODE_CONTAINS = 0x001;              // 0001b
        public static final int MODE_STARTSWITH = 0x002;            // 0010b
        public static final int MODE_SPLIT = 0x004;                 // 0100b
        private static final String SPLIT_SEPARATOR = "[,.\\s]+";  // 分隔符,默认为空白符、英文逗号、英文句号
        private static boolean isFound = false;   // 当MODE_STARTSWITH模式匹配成功时,不再进行MODE_SPLIT模式的匹配
        private int defaultMode = MODE_STARTSWITH;                  // 0110b
    
        private LayoutInflater inflater;
        private ArrayFilter mArrayFilter;
        private ArrayList<String> mOriginalValues;      // 所有的item
        private List<String> mObjects;                  // 过滤后的item
        private final Object mLock = new Object();      // 同步锁
        private int maxMatch = 10;                      // 最多显示的item数目,负数表示全部
        private int simpleItemHeight;                   // 单行item的高度值,故需要在XML中固定父布局的高度值
    
        private char previewChar = '@';                 // 默认字符
        private boolean isSupportPreview = false;       // 是否可以使用@符号进行预览全部提示内容
    
        public AutoCompleteAdapter(Context context, ArrayList<String> mOriginalValues) {
            this(context, mOriginalValues, -1);
        }
    
        public AutoCompleteAdapter(Context context, ArrayList<String> mOriginalValues, int maxMatch) {
            this.mOriginalValues = mOriginalValues;
            // 初始化时将其设置成mOriginalValues,避免在未进行数据保存时执行删除操作导致程序的崩溃
            this.mObjects = mOriginalValues;   
            this.maxMatch = maxMatch;
            inflater = LayoutInflater.from(context);
            initViewHeight();
        }
    
        private void initViewHeight() {
            View view = inflater.inflate(R.layout.simple_dropdown_item_1line, null);
            LinearLayout linearLayout = (LinearLayout) view.findViewById(R.id.layout_item);
            linearLayout.measure(0, 0);
            // 其他方法获取的高度值会因View尚未被绘制而获取到0
            simpleItemHeight = linearLayout.getMeasuredHeight();
        }
    
        public int getSimpleItemHeight() {
            return simpleItemHeight;                // 5 * 2 + 28(dp) => 103(px)
        }
        
        public void setSupportPreview(boolean isSupportPreview){
            this.isSupportPreview = isSupportPreview;
        }
    
        public void setSupportPreview(boolean isSupportPreview, char previewChar){
            this.isSupportPreview = isSupportPreview;
            this.previewChar = previewChar;
        }
    
        @Override
        public int getCount() {
            return mObjects.size();
        }
    
        @Override
        public Object getItem(int position) {
            return mObjects.get(position);
        }
    
        @Override
        public long getItemId(int position) {
            return position;
        }
    
        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            ViewHolder holder;
            if (convertView == null) {
                holder = new ViewHolder();
                convertView = inflater.inflate(R.layout.simple_dropdown_item_1line, null);
                holder.tv = (TextView) convertView.findViewById(R.id.tv_simple_item);
                holder.iv = (ImageView) convertView.findViewById(R.id.iv_simple_item);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.tv.setText(mObjects.get(position));
            holder.iv.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    String value = mObjects.remove(position);
    
                    if (mDeleteListener != null) {
                        mDeleteListener.onSimpleItemDeletedListener(value);
                    }
    
                    if (mFilterListener != null) {
                        mFilterListener.onFilterResultsListener(mObjects.size());
                    }
    
                    mOriginalValues.remove(value);
                    notifyDataSetChanged();
                }
            });
    
            return convertView;
        }
    
        private static class ViewHolder {
            TextView tv;
            ImageView iv;
        }
    
        public void setDefaultMode(int defaultMode) {
            this.defaultMode = defaultMode;
        }
    
        public void add(String item) {
            mOriginalValues.add(item);
            notifyDataSetChanged();         //
        }
    
        public void clear() {
            if(mOriginalValues != null && !mOriginalValues.isEmpty()) {
                mOriginalValues.clear();
                notifyDataSetChanged();         //
            }
        }
    
        // Interface
        public interface OnSimpleItemDeletedListener {
            void onSimpleItemDeletedListener(String value);
        }
    
        private OnSimpleItemDeletedListener mDeleteListener;
    
        public void setOnSimpleItemDeletedListener(OnSimpleItemDeletedListener listener) {
            this.mDeleteListener = listener;
        }
    
        // Interface
        public interface OnFilterResultsListener {
            void onFilterResultsListener(int count);
        }
    
        private OnFilterResultsListener mFilterListener;
    
        public void setOnFilterResultsListener(OnFilterResultsListener listener) {
            this.mFilterListener = listener;
        }
    
        @Override
        public Filter getFilter() {
            if (mArrayFilter == null) {
                mArrayFilter = new ArrayFilter(mFilterListener);
            }
            return mArrayFilter;
        }
    
        private class ArrayFilter extends Filter {
    
            private OnFilterResultsListener listener;
    
            public ArrayFilter(OnFilterResultsListener listener) {
                this.listener = listener;
            }
    
            @Override
            protected FilterResults performFiltering(CharSequence prefix) {
                FilterResults results = new FilterResults();
    
                if (mOriginalValues == null) {
                    synchronized (mLock) {
                        mOriginalValues = new ArrayList<>(mObjects);
                    }
                }
    
                if (prefix == null || prefix.length() == 0) {
                    synchronized (mLock) {
                        ArrayList<String> list = new ArrayList<>(mOriginalValues);
                        results.values = list;
                        results.count = list.size();
                    }
                } else {
                    if (isSupportPreview) {
                        int index = prefix.toString().indexOf(String.valueOf(previewChar));
                        if (index != -1) {
                            prefix = prefix.toString().substring(index + 1);
                        }
                    }
                
                    String prefixString = prefix.toString().toLowerCase();      // prefixString
                    final int count = mOriginalValues.size();                   // count
                    final ArrayList<String> newValues = new ArrayList<>(count); // newValues
    
                    for (int i = 0; i < count; i++) {
                        final String value = mOriginalValues.get(i);            // value
                        final String valueText = value.toLowerCase();           // valueText
    
                        // 1. 匹配所有
                        if ((defaultMode & MODE_CONTAINS) != MODE_NONE) {
                            if (valueText.contains(prefixString)) {
                                newValues.add(value);
                            }
                        } else {    // support: defaultMode = MODE_STARTSWITH | MODE_SPLIT
                            // 2. 匹配开头
                            if ((defaultMode & MODE_STARTSWITH) != MODE_NONE) {
                                if (valueText.startsWith(prefixString)) {
                                    newValues.add(value);
                                    isFound = true;
                                }
                            }
                            // 3. 分隔符匹配,效率低
                            if (!isFound && (defaultMode & MODE_SPLIT) != MODE_NONE) {
                                final String[] words = valueText.split(SPLIT_SEPARATOR);
                                for (String word : words) {
                                    if (word.startsWith(prefixString)) {
                                        newValues.add(value);
                                        break;
                                    }
                                }
                            }
                            if(isFound) {   // 若在MODE_STARTSWITH模式中匹配,则再次复位进行下一次判断
                                isFound = false;
                            }
                        }
    
                        if (maxMatch > 0) {             // 限制显示item的数目
                            if (newValues.size() > maxMatch - 1) {
                                break;
                            }
                        }
                    } // for (int i = 0; i < count; i++)
                    results.values = newValues;
                    results.count = newValues.size();
                }
    
                return results;
            }
    
            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                //noinspection unchecked
                mObjects = (List<String>) results.values;
    
                if (results.count > 0) {
                    // 由于当删除提示框中的记录行时,而AutoCompleteTextView此时内容又不改变,故不会触发FilterResults事件
                    // 导致删除记录行时,提示框的高度不会发生相应的改变
                    // 解决方法:需要在ImageView的点击监听器中也调用OnFilterResultsListener.onFilterResultsListener()
                    // 来共同完成
                    if (listener != null) {
                        listener.onFilterResultsListener(results.count);
                    }
                    notifyDataSetChanged();
                } else {
                    notifyDataSetInvalidated();
                }
            }
        }
    }
    
    • 下拉提示框的item布局simple_dropdown_item_1line.xml

    这里需要固定父类控件LinearLayout的高度,在AutoCompleteAdapter中会获取其高度用于设置AutoCompleteTextView的下拉菜单的高度

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/layout_item"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="28dp"
        android:padding="5dp"
        android:gravity="center_vertical">
    
        <TextView
            android:id="@+id/tv_simple_item"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:paddingStart="5dp"
            android:paddingEnd="0dp"
            android:text="@string/text_nothing"
            android:textAllCaps="false"
            android:textSize="18sp"
            android:textColor="#000"/>
    
        <ImageView
            android:id="@+id/iv_simple_item"
            android:layout_width="18dp"
            android:layout_height="18dp"
            android:layout_marginEnd="5dp"
            android:src="@drawable/ic_action_name"
            android:contentDescription="@null"
            android:scaleType="fitCenter" />
    </LinearLayout>
    
    • 效果演示
    • 支持使用@字符来预览所有提示内容


    展开全文
  • layout方法的简单使用案例

    千次阅读 2015-12-20 11:49:35
    layout的妙用
    layout代表着某一个view在父view中的位置,比如
    ViewGroup parent = new ViewGroup();
    View child = new View();
    parent.addView(child)
    child.layout(l,t,r,b);
    ps:用new 来初始化对象只是为了方便说明情况
    此时child是parent的子View,child.layout(l,t,r,b)就说明了child在parent放置的坐标位置,坐标原点(0,0)就是parent的左上角。

    其中(l,t)代表着左上角的位置,(r,b)代表着右下角的位置。

    使用案例,如下这个页面:该页面有多个view可以获取焦点,并在获取到焦点的view上赋予一个焦点框。


    刚开始实现这个页面的时候使用切图的方法,也就是把每个view的焦点框切一个图,放在

    <selector xmlns:android="http://schemas.android.com/apk/res/android" >
        <!--focus_img:焦点框图片-->
        <item android:state_focused="true" android:drawable="@drawable/focused_img"/>
        <item android:state_selected="true" android:drawable="@drawable/focused_img"/>
        <item android:state_pressed="true" android:drawable="@drawable/focused_img"></item>
        <item android:drawable="@drawable/unfocused_img"/>
    </selector>

    这中配置文件来让一个设置一个view的background.但是这样做有一个问题:这个页面有几个不同大小的view需要焦点框效果。也就是说要让美工针对不同的view来切不同的焦点框。要知道这只是应用的一个界面,还有其他的页面同样需要获取焦点框,那么就还得让美工切图。这样的话就得为每个焦点框配置一个selector这种重复性的工作。
    下面就用另外一种方法来,该是layout出场的时候了:

    思路:
    1)提供一个.9图片的焦点框(当然焦点框也可以用shape来实现焦点框)


    2)提供一个ImageView,这个ImageView的src就是.9图片,假设该ImageView的变量名为focusImgView
    3)对页面focusable为true的view设置FoncusChangeListener,如果某个view获取焦点的话,获取该view的左上角的位置(x,y)和它的宽度(width,height).然后设置
    focusImgView.layout(x,y,x+width,y+height)(因为android中没有直接的方法来获取某个view右下角的位置,所以需要通过宽和高来计算之)

    基本的代码如下:

    /**焦点框View**/
    public class FocusView extends ViewGroup{
        /**焦点框**/
    	private ImageView focusImgView;
    	/**页面中获取焦点的view**/
    	private View focusView;	
    	/*焦点.9图片*/
    	private Drawable mViewFocusDrawable;
    	public FocusView(Context context,Drawable mViewFocusDrawable) {
    		super(context);
    		init(mViewFocusDrawable);
    	}
    	
    	
    	private void init(Drawable mViewFocusDrawable) {
    		focusImgView = new  ImageView(getContext());
    		ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    		addView(focusImgView, params);
    		focusImgView.setBackgroundDrawable(mViewFocusDrawable);
    	}
    
    	/**
    	 * 设置焦点的图片
    	 * @param d
    	 */
    	public void setFocusDrawable(Drawable mViewFocusDrawable) {
    		focusImgView.setBackgroundDrawable(mViewFocusDrawable);
    	}
    	
    	/*
    	*@focusView 获取焦点的view
    	*/
    	public void showFocus(View focusView) {
    		
    		int[] location = new int[2];
    		focusView.getLocationInWindow(location);
    		int x = location[0];
    		int y = location[1];
    		int width = focusView.getWidth();
    		int height = focusView.getHeight();
    	        //这是重点步骤
    		focusImgView.layout(x, y,x+ width,y + height);
    	}
    
    
    	@Override
    	protected void onLayout(boolean changed, int l, int t, int r, int b) {
    	}
    	
    }
    
    在onFoucusChange方法里面这么调用:
    	@Override
    	public void onFocusChange(View view, boolean hasfocus) {
    
    		int viewId = view.getId();
    		switch (viewId) {
    		case R.id.demo_btn:
    			if (hasfocus) {
    				mFocusView.showFocus(view);
    			}
    			break;
    		}
    	}

    这样一个简单的.9图片就能够满足页面的应用:其他页面使用的效果图:


    展开全文
  • 自定义View之Layout方法详解

    千次阅读 2018-05-29 13:30:09
    自定义View之onLayout、Layout分析

    如果你喜欢讨论源码,一起加入群:524727903

    在ViewGroup中会调用onLayout方法(在ViewGroup类中是抽象的,在子元素中实现,一会会用LinearLayout进行举例子)去遍历所有的子元素,并调用其layout方法,在layout方法中又会调用子元素的onLayout方法。

    LinearLayout的onLayout方法

    @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            if (mOrientation == VERTICAL) {
                layoutVertical(l, t, r, b);
            } else {
                layoutHorizontal(l, t, r, b);
            }
        }

    可以看到与onMeasure方法类似,分为水平方向和垂直方向,我们只分析一下垂直方向的源码:

     void layoutVertical(int left, int top, int right, int bottom) {
            final int paddingLeft = mPaddingLeft;
    
            int childTop;
            int childLeft;
    
            // Where right end of child should go
            final int width = right - left;
            int childRight = width - mPaddingRight;
    
            // Space available for child
            int childSpace = width - paddingLeft - mPaddingRight;
    
            final int count = getVirtualChildCount();
    
            final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
            final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    
            switch (majorGravity) {
               case Gravity.BOTTOM:
                   // mTotalLength contains the padding already
                   childTop = mPaddingTop + bottom - top - mTotalLength;
                   break;
    
                   // mTotalLength contains the padding already
               case Gravity.CENTER_VERTICAL:
                   childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
                   break;
    
               case Gravity.TOP:
               default:
                   childTop = mPaddingTop;
                   break;
            }
    
            for (int i = 0; i < count; i++) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    childTop += measureNullChild(i);
                } else if (child.getVisibility() != GONE) {
                    final int childWidth = child.getMeasuredWidth();
                    final int childHeight = child.getMeasuredHeight();
    
                    final LinearLayout.LayoutParams lp =
                            (LinearLayout.LayoutParams) child.getLayoutParams();
    
                    int gravity = lp.gravity;
                    if (gravity < 0) {
                        gravity = minorGravity;
                    }
                    final int layoutDirection = getLayoutDirection();
                    final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                    switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                        case Gravity.CENTER_HORIZONTAL:
                            childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                    + lp.leftMargin - lp.rightMargin;
                            break;
    
                        case Gravity.RIGHT:
                            childLeft = childRight - childWidth - lp.rightMargin;
                            break;
    
                        case Gravity.LEFT:
                        default:
                            childLeft = paddingLeft + lp.leftMargin;
                            break;
                    }
    
                    if (hasDividerBeforeChildAt(i)) {
                        childTop += mDividerHeight;
                    }
    
                    childTop += lp.topMargin;
                    setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                            childWidth, childHeight);
                    childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    
                    i += getChildrenSkipCount(child, i);
                }
            }
        }
    
    

    大家可以看到在此方法中遍历所有的子View,并对其进行测量final int childHeight = child.getMeasuredHeight(); 最后有一个childTop变量每次都进行累加

     childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

    这也正好符合我们LinearLayout的垂直方向的布局。最后调用

    setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);
     private void setChildFrame(View child, int left, int top, int width, int height) {        
            child.layout(left, top, left + width, top + height);
        }

    到这里我们就明白了整个View的onLayout过程,接着会调用子类的layout方法:

    public void layout(int l, int t, int r, int b) {
            if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
                onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
    
            int oldL = mLeft;
            int oldT = mTop;
            int oldB = mBottom;
            int oldR = mRight;
    
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnLayoutChangeListeners != null) {
                    ArrayList<OnLayoutChangeListener> listenersCopy =
                            (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                    int numListeners = listenersCopy.size();
                    for (int i = 0; i < numListeners; ++i) {
                        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                    }
                }
            }
    
            mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
            mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        }

    主要说一下这段代码

    boolean changed = isLayoutModeOptical(mParent) ?
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    isLayoutModeOptical

    Return true if o is a ViewGroup that is laying out using optical bounds.

      public static boolean isLayoutModeOptical(Object o) {
            return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
        }
        boolean isLayoutModeOptical() {
        return mLayoutMode == LAYOUT_MODE_OPTICAL_BOUNDS;
        }

    这句话主要是判断LayoutMode的模式是不是为LAYOUT_MODE_OPTICAL_BOUNDS,如果为真就会运行setOpticalFrame(l, t, r, b)否则就会运行setFrame(l, t, r, b)

    首先我们要知道

    private int mLayoutMode = LAYOUT_MODE_UNDEFINED;

    mLayoutMode的值默认是LAYOUT_MODE_UNDEFINED

    也就是说:”mLayoutMode == LAYOUT_MODE_OPTICAL_BOUNDS”默认是返回false

    This constant is a layoutMode. Optical bounds describe where a widget appears to be. They sit inside the clip bounds which need to cover a larger area to allow other effects, such as shadows and glows, to be drawn.

    这里有注释,大致意思是说,设置完这个属性之后,在布局的时候,View需要放在一个较大区域的布局内,以便留出来阴影之类位置(在后面代码的部分会有讲解)。

    可以通过setLayoutMode方法,手动设置成ViewGroup.LAYOUT_MODE_OPTICAL_BOUNDS

    另外

    setOpticalFrame(l, t, r, b)
    setFrame(l, t, r, b);

    这两个方法的源码在后面讲解


    先看看整个项目代码:

    ==MainActivity==

    public class MainActivity extends AppCompatActivity{
    
        private ViewPager mViewPager;
        private MyFragmentPagerAdapter myFragmentPagerAdapter;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            LinearLayout root = (LinearLayout) findViewById(R.id.root);
    
            ViewGroup parent = (ViewGroup) root.getParent();
    
            parent.setLayoutMode(ViewGroup.LAYOUT_MODE_OPTICAL_BOUNDS);
            try {
                //通过反射,拿到私有的Insets类。
                Class classz = Class.forName("android.graphics.Insets");
                 /*以下调用带参的、私有构造函数*/
                Constructor c1=classz.getDeclaredConstructor(new Class[]{int.class,int.class,int.class,int.class});
                c1.setAccessible(true);
                //初始化4个int型参数
                Object o = c1.newInstance(new Object[]{50,50,50,50});
                View view=parent;
                Class<? extends View> viewClassz = view.getClass();
                //得到Insets的setOpticalInsets方法
                Method setOpticalInsets = viewClassz.getMethod("setOpticalInsets", o.getClass());
                setOpticalInsets.invoke(view,o);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    ==布局文件==

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/root"
        android:elevation="10dp"
        android:background="#fff"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
    
        <Button
            android:layout_width="match_parent"
            android:layout_height="10dp"
        >
    
    </LinearLayout>

    当isLayoutModeOptical返回true的时候,会运行setOpticalFrame

    private boolean setOpticalFrame(int left, int top, int right, int bottom) {
            Insets parentInsets = mParent instanceof View ?
                    ((View) mParent).getOpticalInsets() : Insets.NONE;
            Insets childInsets = getOpticalInsets();
            return setFrame(
            left   + parentInsets.left - childInsets.left,
            top    + parentInsets.top  - childInsets.top,
            right  + parentInsets.left + childInsets.right,
            bottom + parentInsets.top  + childInsets.bottom);
        }
    在这里很容易发现,setOpticalFrame最后还是调用了setFrame方法,只不过对左上右下的值进行了重新的设置。

    这里要将一下Insets类

    /*
     * Copyright (C) 2006 The Android Open Source Project
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package android.graphics;
    
    /**
     * An Insets instance holds four integer offsets which describe changes to the four
     * edges of a Rectangle. By convention, positive values move edges towards the
     * centre of the rectangle.
     * <p>
     * Insets are immutable so may be treated as values.
     *
     * @hide
     */
    public class Insets {
        public static final Insets NONE = new Insets(0, 0, 0, 0);
    
        public final int left;
        public final int top;
        public final int right;
        public final int bottom;
    
        private Insets(int left, int top, int right, int bottom) {
            this.left = left;
            this.top = top;
            this.right = right;
            this.bottom = bottom;
        }
    
        // Factory methods
    
        /**
         * Return an Insets instance with the appropriate values.
         *
         * @param left the left inset
         * @param top the top inset
         * @param right the right inset
         * @param bottom the bottom inset
         *
         * @return Insets instance with the appropriate values
         */
        public static Insets of(int left, int top, int right, int bottom) {
            if (left == 0 && top == 0 && right == 0 && bottom == 0) {
                return NONE;
            }
            return new Insets(left, top, right, bottom);
        }
    
        /**
         * Return an Insets instance with the appropriate values.
         *
         * @param r the rectangle from which to take the values
         *
         * @return an Insets instance with the appropriate values
         */
        public static Insets of(Rect r) {
            return (r == null) ? NONE : of(r.left, r.top, r.right, r.bottom);
        }
    
        /**
         * Two Insets instances are equal iff they belong to the same class and their fields are
         * pairwise equal.
         *
         * @param o the object to compare this instance with.
         *
         * @return true iff this object is equal {@code o}
         */
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
    
            Insets insets = (Insets) o;
    
            if (bottom != insets.bottom) return false;
            if (left != insets.left) return false;
            if (right != insets.right) return false;
            if (top != insets.top) return false;
    
            return true;
        }
    
        @Override
        public int hashCode() {
            int result = left;
            result = 31 * result + top;
            result = 31 * result + right;
            result = 31 * result + bottom;
            return result;
        }
    
        @Override
        public String toString() {
            return "Insets{" +
                    "left=" + left +
                    ", top=" + top +
                    ", right=" + right +
                    ", bottom=" + bottom +
                    '}';
        }
    }
    

    这个类很简单,主要是通单例模式得到Insets

    An Insets instance holds four integer offsets which describe changes to the four edges of a Rectangle. By convention, positive values move edges towards the centre of the rectangle.
    Insets are immutable so may be treated as values.

    通过这段注释大致了解到Insets其实就是封装了四个参数的偏移量,只不过这个类是hide(隐藏),所以上面的代码通过反射得到Insets,之后看效果图,就能明白这个类的大致含义了。

    同样在View类中setOpticalInsets也是hide模式的,所以我们要转换成View对象,然后接着用反射,去setOpticalInsets(代码参上)。

    接着


    protected boolean setFrame(int left, int top, int right, int bottom) {
            boolean changed = false;
    
            if (DBG) {
                Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                        + right + "," + bottom + ")");
            }
    
            if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
                changed = true;
    
                // Remember our drawn bit
                int drawn = mPrivateFlags & PFLAG_DRAWN;
    
                int oldWidth = mRight - mLeft;
                int oldHeight = mBottom - mTop;
                int newWidth = right - left;
                int newHeight = bottom - top;
                boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
    
                // Invalidate our old position
                invalidate(sizeChanged);
    
                mLeft = left;
                mTop = top;
                mRight = right;
                mBottom = bottom;
                mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    
                mPrivateFlags |= PFLAG_HAS_BOUNDS;
    
    
                if (sizeChanged) {
                    sizeChange(newWidth, newHeight, oldWidth, oldHeight);
                }
    
                if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                    // If we are visible, force the DRAWN bit to on so that
                    // this invalidate will go through (at least to our parent).
                    // This is because someone may have invalidated this view
                    // before this call to setFrame came in, thereby clearing
                    // the DRAWN bit.
                    mPrivateFlags |= PFLAG_DRAWN;
                    invalidate(sizeChanged);
                    // parent display list may need to be recreated based on a change in the bounds
                    // of any child
                    invalidateParentCaches();
                }
    
                // Reset drawn bit to original value (invalidate turns it off)
                mPrivateFlags |= drawn;
    
                mBackgroundSizeChanged = true;
                if (mForegroundInfo != null) {
                    mForegroundInfo.mBoundsChanged = true;
                }
    
                notifySubtreeAccessibilityStateChangedIfNeeded();
            }
            return changed;
        }

    这时,如果布局改变之后changed为true,还会回调

     sizeChange(newWidth, newHeight, oldWidth, oldHeight);

    这里面是一个空实现,我们可以根据逻辑进行添加,最后方法返回changed的值。


    紧接着

     if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnLayoutChangeListeners != null) {
                    ArrayList<OnLayoutChangeListener> listenersCopy =
                            (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                    int numListeners = listenersCopy.size();
                    for (int i = 0; i < numListeners; ++i) {
                        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                    }
                }
            }

    当布局改变changd为true。mPrivateFlags 这个属性之后讲。
    会调用

    onLayout(changed, l, t, r, b)

    方法,主要是用于布局所用,你可以自定义ViewGroup,在这里根据逻辑设置你想要的布局。


    最后:

    上图:

    这里写图片描述

    ==可以看到上下左右都空出了些距离,这就是通过Insets的构造函数产生的!==

    如果你的View对象添加了

    addOnLayoutChangeListener(this);

    然后逻辑就很简单了,用过ArrayList把回调的复制一份,然后遍历,最后调用onLayoutChange方法,里面传的当前View和现在的左上右下和之前的左上右下。

    @Override
        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
            System.out.println("v = " + v);
            System.out.println("left = " + left);
            System.out.println("top = " + top);
            System.out.println("right = " + right);
            System.out.println("bottom = " + bottom);
            System.out.println("oldLeft = " + oldLeft);
            System.out.println("oldTop = " + oldTop);
            System.out.println("oldRight = " + oldRight);
            System.out.println("oldBottom = " + oldBottom);
            System.out.println("\"----------------------------\" = " + "----------------------------");
        }
    展开全文
  • vue、Layout 布局、Layout 属性、vue Layout 全部布局、vue Layout 全部属性
  • 边界布局管理器把容器的的布局分为五个位置:CENTER、EAST、WEST、NORTH、SOUTH。依次对应为:上北(NORTH)、下南(SOUTH)、左西(WEST)、右东(EAST),中(CENTER),如下图所示。 特征: ...
  • Unity/Auto Layout -- 理解Layout Elements(布局元素)

    万次阅读 多人点赞 2018-03-13 17:12:52
    前言 在UGUI1中,Canvas下的每个GameObject都会自动添加 Rect Transform 组件来控制自身的位置和大小。通常情况下,基于Rect Transform的布局系统已经足够灵活,可以方便地满足大部分UI布局的需要。...
  • Android系统布局——android.R.layout详解

    千次阅读 2018-09-23 00:31:03
    布局文件,作为android中...系统布局文件:android.R.layout.xxx; 用户自定义布局文件:R.layout.xxx; 那系统布局文件究竟有哪一些,大家在用的时候如果不了解,心里估计有点惴惴。现在下方图中列出所有系统布局,...
  • Android新特性介绍,ConstraintLayout完全解析

    万次阅读 多人点赞 2017-05-11 16:07:51
    今天给大家带来2017年的第一篇文章,这里先祝大家新年好。 本篇文章的主题是ConstraintLayout。其实ConstraintLayout是Android Studio 2.2中主要的新增功能之一,也是Google在去年的I/O大会上重点宣传的一个功能。...
  • Vlayout使用详细介绍

    千次阅读 2018-10-17 18:38:20
    Vlayout使用详细介绍 目录介绍 1.Vlayout简单介绍 2.主要功能介绍 2.1 主要功能的思维导图 2.2 主要功能说明 .使用方法与案例 3.1 初始化 3.2 设置回收复用池 ...4.1 VirtualLayoutAdapte
  • R语言中layout()函数的用法

    万次阅读 2018-04-19 09:49:34
    R语言中layout()函数的用法
  • gdb调试的layout使用

    万次阅读 2013-08-21 16:41:24
    layout:用于分割窗口,可以一边查看代码,一边测试。主要有以下几种用法: layout src:显示源代码窗口 layout asm:显示汇编窗口 layout regs:显示源代码/汇编和寄存器窗口 layout split:显示源代码和汇编窗口 ...
  • 在进行UI布局的时候,可能经常会用到 android:gravity 和 android:layout_Gravity 这两个属性。 关于这两个属性的区别,网上已经有很多人进行了说明,这边再简单说一下。 (资料来自网络) LinearLayout有两个非常...
  • android:layout_weight的真实含义

    万次阅读 多人点赞 2014-04-28 21:02:23
    之所以android:layout_weight会引起争议,是因为在设置该属性的同时,设置android:layout_width为wrap_content和match_parent会造成两种截然相反的效果。如下所示: android:layout_width="match_parent
  • 新建MVC的项目时会自动创建_ViewStart.cshtml、_Layout.cshtml页面,这样我们就把公共的部分放到_Layout页面中,每次新建页面时都会自动加载_ViewStart。 _ViewStart.cshtml页面代码: @{ Layout = "~/Views/...
  • ASPMVC- Layout 使用

    千次阅读 2018-07-03 09:41:31
    在ASP MVC模式中,采用Layout策略实现了网站主体布局的搭建,采用Layout后,应用系统只需要专心于各种业务页面的开发,通过Layout实现系统布局,完成各种页面的切换。在aspmvc框架下,默认模板中,采用Layout策略时...
  • Layout,翻译为中文的意思是 布局,安排,版面设计。对于许多的组件的命令,都有Layout_x和x 的区别,而许多的区别很明显,直接是Layout是相对于父容器(一般就是整个xml的布局)的改变,一个是相对于组件本身的...
  • vue项目实战(二)之首页layout布局

    万次阅读 2018-11-09 09:18:41
    1.在 src/components目录下新建Layout.vue文件: &lt;template&gt; &lt;div&gt; &lt;h2&gt;header&lt;/h2&gt; &lt;h2&gt;content&lt;/h2&gt; &lt;h2&...
  • android:layout_gravity和android:gravity的区别

    万次阅读 多人点赞 2012-08-08 15:53:04
    1.首先来看看android:layout_gravity和android:gravity的使用区别。 android:gravity: 这个是针对控件里的元素来说的,用来控制元素在该控件里的显示位置。例如,在一个Button按钮控件中设置如下两个属性, ...
  • layout_marginStart和layout_marginEnd

    万次阅读 2017-05-09 16:02:54
    8.4.6 从右到左布局(RTL Layout) 从Android 4.2开始,Android SDK支持一种从右到左(RTL,Right-to-Left)UI布局的方式,尽管这种布局方式经常被使用在诸如阿拉伯语、希伯来语等环境中,中国用户很少使用。...
1 2 3 4 5 ... 20
收藏数 875,990
精华内容 350,396
关键字:

layout