Day05 01.昨天内容总结 ##
Day05 02.应用详情页-截图模块 ##
首页详情页 ##
(详情页-图片信息模块)DetailAppPicsHolder
图片信息模块是一张一张的图片,而且可以左右的滑动,看起来好像是一个ViewPager
但实际上在这个地方,我们不用ViewPager
在哪种情况下我们用ViewPager用的比较多呢
比如说宽度(一张图片的宽度)它是填充屏幕的,来回去滑动
比如首页轮播图和项目整体的首页 应用 游戏 专题 推荐等等它们都是填充屏幕,
所以整体架构时 也是ViewPager
我们这里是宽度不是填充屏幕的,而是一小段一小段的,当然如果这个必须用ViewPager去实现的话,也能实现,不过就是稍微有点麻烦,我们在这个地方也用不着ViewPager, 它只是左右去滑,
也没有什么点击之后切换页面的效果 它看起来更像一个横向的ScrollView,我们现在应用详情页现在纵向是一个ScrollView,而这个图片信息模块像一个横向的ScrollView
实际上我们android中是有一个横向的ScrollView,专门去处理我们这个横向的布局
在layout_home_detail.xml布局文件中增加横向的ScrollView来占位置并给它设置背景
layout_home_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:layout_marginLeft="5dp"
android:orientation="vertical"
<!-- 应用信息 -->
<FrameLayout
android:id="@+id/fl_detail_appinfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_item_bg_selector"
/>
<!-- 安全信息 -->
<FrameLayout
android:id="@+id/fl_detail_safeinfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_item_bg_selector"
/>
<!-- 应用截图 -->
<HorizontalScrollView
android:id="@+id/hsv_detail_picinfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#9e9e9e"
/>
</LinearLayout>
</ScrollView>
---------------------------------------------
然后再来到HomeDetailActivity.java中,填充应用截图数据
HomeDetailActivity.java
//填充应用截图数据
HorizontalScrollView hsvPicInfo = (HorizontalScrollView)view
.findViewById(R.id.hsv_detail_picinfo);
DetailPicInfoHolder picInfoHolder = new DetailPicInfoHolder();
hsvPicInfo.addView(picInfoHolder.getRootView());
picInfoHolder.setData(data);
注意:
hsvScreen findViewById之后,就可以给hsvScreen添加布局了
我们去写一个holder对象来封装它的布局
---------------------------------------------------------------
Day05 03.应用详情页-描述模块01 ##
/**
-
详情页-应用图片
* -
@author Kevin
*/
public class DetailPicInfoHolder extends BaseHolder {private ImageView[] mImageViews; private BitmapUtils mBitmapUtils; @Override public View initView() { View view = View.inflate(UIUtils.getContext(), R.layout.layout_detail_picinfo, null); mImageViews = new ImageView[5]; mImageViews[0] = (ImageView) view.findViewById(R.id.iv_pic1); mImageViews[1] = (ImageView) view.findViewById(R.id.iv_pic2); mImageViews[2] = (ImageView) view.findViewById(R.id.iv_pic3); mImageViews[3] = (ImageView) view.findViewById(R.id.iv_pic4); mImageViews[4] = (ImageView) view.findViewById(R.id.iv_pic5); mBitmapUtils = BitmapHelper.getBitmapUtils(); return view; } @Override public void refreshView(AppInfo data) { if (data != null) { ArrayList<String> list = data.screen; for (int i = 0; i < 5; i++) { if (i < list.size()) { mImageViews[i].setVisibility(View.VISIBLE); mBitmapUtils.display(mImageViews[i], HttpHelper.URL
“image?name=” + list.get(i));
} else {
mImageViews[i].setVisibility(View.GONE);
}
}
}
}
}
注意:
initView初始化控件完了之后,我们就在refreshView方法中开始填充数据
要填充数据的话,有缘网是5张图片,但是有些界面它的截图的数量不会很多,可能是3张或4张
如果真的遇到3张或4张这种情况的话,那这时候我们就要把多余的图片隐藏掉
-----------------------------------------------------------------
应用详情页-应用截图的布局文件
layout_detail_picinfo.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:orientation="horizontal" >
<ImageView
android:id="@+id/iv_pic1"
android:layout_width="90dp"
android:layout_height="150dp"
android:layout_marginLeft="3dp" />
<ImageView
android:id="@+id/iv_pic2"
android:layout_width="90dp"
android:layout_height="150dp"
android:layout_marginLeft="3dp" />
<ImageView
android:id="@+id/iv_pic3"
android:layout_width="90dp"
android:layout_height="150dp"
android:layout_marginLeft="3dp" />
<ImageView
android:id="@+id/iv_pic4"
android:layout_width="90dp"
android:layout_height="150dp"
android:layout_marginLeft="3dp" />
<ImageView
android:id="@+id/iv_pic5"
android:layout_width="90dp"
android:layout_height="150dp"
android:layout_marginLeft="3dp" />
</LinearLayout>
注意:
我们目前在写这个布局的时候,怎么去写的呢,我们去写的是一个一个的ImageView
它一个屏幕肯定是展现的不够,它就会超出这个屏幕,但是因为我将这个整体的布局我塞在了
一个横向滑动的ScrollView中来了,那这时候他就不会超出屏幕,而是可以滑动了
其实和纵向的ScrollView一个道理,纵向的可能写了好多布局,屏幕排不开
这时候你用一个大大的ScrollView作为它的控件,把它包起来 它就能够向下滑动了
-------------------------------------------------------------------
Day05 04.应用详情页-描述模块02 ##
(详情页-应用描述模块)
DetailDesInfoHolder.java
/**
-
应用详情页-应用描述
-
@author Brant
*
*/
public class DetailDesInfoHolder extends BaseHolder {private TextView tvDetailDes; private RelativeLayout rlDetailToggle; private TextView tvDetailAuthor; private ImageView ivArrow; @Override public View initView() { View view = UIUtils.inflate(R.layout.layout_detail_desinfo); tvDetailDes = (TextView) view.findViewById(R.id.tv_detail_des); rlDetailToggle = (RelativeLayout) view.findViewById(R.id.rl_detail_toggle); tvDetailAuthor = (TextView) view.findViewById(R.id.tv_detail_author); ivArrow = (ImageView) view.findViewById(R.id.iv_arrow); return view; } @Override public void refreshView(AppInfo data) { tvDetailDes.setText(data.des); tvDetailAuthor.setText(data.author); } }
应用详情页-应用描述布局
layout_detail_desinfo.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="应用介绍" android:textColor="#9e9e9e" android:textSize="16sp" /> <TextView android:id="@+id/tv_detail_des" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#9e9e9e" android:textSize="14sp" /> <RelativeLayout android:id="@+id/rl_detail_toggle" android:layout_width="match_parent" android:layout_height="wrap_content" > <TextView android:id="@+id/tv_detail_author" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:textColor="#000" android:textSize="14sp" /> <ImageView android:id="@+id/iv_arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:src="@drawable/arrow_down" /> </RelativeLayout> </LinearLayout>
写完应用描述信息DetailDesInfoHolder之后
要把它添加到主页面HomeDetailActivity中,
打开主页面布局layout_home_detail.xml
现在已经有了三个模块 一个是应用信息 一个是安全信息 一个是截图
那我们再写一个帧布局来填充我们的描述信息
layout_home_detail.xml
<?xml version="1.0" encoding="utf-8"?><ScrollView
xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“match_parent”
android:layout_height=“match_parent” >
<LinearLayout
android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginRight="5dp" android:layout_marginLeft="5dp" android:orientation="vertical" <!-- 应用信息 --> <FrameLayout android:id="@+id/fl_detail_appinfo" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/list_item_bg_selector" /> <!-- 安全信息 --> <FrameLayout android:id="@+id/fl_detail_safeinfo" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/list_item_bg_selector" /> <!-- 应用截图 --> <HorizontalScrollView android:id="@+id/hsv_detail_picinfo" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#9e9e9e" /> <!-- 描述信息 --> <FrameLayout android:id="@+id/fl_detail_desinfo" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="3dp" android:background="@drawable/list_item_bg_selector" />
上边将描述信息的布局写好之后,就可以来到HomeDetailActivity中
的onCreateSuccessView来,填写我们的描述信息
HomeDetailActivity.java
//填充应用描述数据 FrameLayout rlDesInfo = (FrameLayout) view.findViewById(R.id.fl_detail_desinfo); DetailDesInfoHolder desInfoHolder = new DetailDesInfoHolder(); rlDesInfo.addView(desInfoHolder.getRootView()); desInfoHolder.setData(data);
运行程序 布局成功展示出来了,接下来分析下,我们目前的布局是把完整的效果展示出来了
实际上呢,它默认刚进来的时候,比如说有缘网,对这个应用描述数据的textView进行了一个
截断,它的高度只展示7行的内容,只有你去往下展开的时候,它才会把剩余的行数展示出来
然后在点击一下 它又把7行之外的文字隐藏, 当时在做官方,安全,无广告的时候,那个好做,
要么从0展现到完整的布局,要么从完整的布局展现到0
这个地方的话,它并不是从0的高的变为展开,而是从7行textView的地方开始展开到完整的布局
从完整的布局开始隐藏到7行的TextView的这样一个高度,所以的话,它现在的高度分两种,
一种是7行的这样一种比较短的高度,一种是比较长的高度,
所以我们这时候可以去定义一下这样的高度,看看如何去获取它,
先在DetailDesInfoHolder.java中的成员变量处定义一下 即:
private int mShortHeight; //7行较短高度 private int mLongHeight; //完整的较长高度
当我们点击应用描述条目的时候,这个条目就要开始变化,我们给他设置一个点击监听
即:rlDetailToggle.setOnClickListener
当被点击的时候,我们就要展开或隐藏,我们在这也写一个toggle方法 来写它展开或隐藏的状态
首先我们要判断它当前到底是展开还是收起,所以在写一个变量来记录它
即: private boolean isOpen = false;
在toggle方法中判断 然后它需要一个动画器 ValueAnimator,并在if和else中初始化,
即ValueAnimator.ofInt();
初始化完了之后,就可以给这个Animator去设置一个addUpdateListener去更新布局
从arg0中就可以拿到它的动画的一个最新的结果,即Integer类型的变量height
这时候就可以把它设置给我们的高度
找一下我们的TextView,tvDetailDes就是我们的TextView,那这个textview的话,
我们在初始化界面的时候呢(即RefreshView方法) 我们就可以去获取它的布局参数:
即:
LayoutParams mParams = tvDetailDes.getLayoutParams();
并把它设置成全局的变量,这时候就可以在Toggle方法的监听中修改下它的参数,即:
mParams.height 等于下我们上边的最新的height
同时的话,将我们的参数设置给tvDetailDes布局,即:
tvDetailDes.setLayoutParams(mParams);
最后我们可以把这个布局直接的设置运行时间,并启动这个动画
这是我们动画的这样一个逻辑。
接下来我们再去分析下这个,mShortHeight和mLongHeight,到底应该怎么去初始化,
我们先分析下这个mShortHeight,我们第一次进入程序的应用详情页的时候,应用描述信息
它展现的高度就应该是mShortHeight的高度,那我怎么知道这个7行的高度到底是多高呢,
因为这个TextView字体大小包括行间距都不确定,这里专门去写一个方法getShortHeight,去获取下这个7行的TextView到底有多高
我们可以模拟一个TextView,看它能有多高,即在getShortHeight方法中模拟一个TextView,设置
该TextView最大行数为7,即TextView有个方法,可以设置最大行为7,然后计算此TextView的高度,
而这个高度就是我们7行的TextView的高度
即在getShortHeight方法中,new一个TextView,给这个view设置text,模拟假的TextView,而这个TextView的话,它的文字肯定也是我们应用描述的信息,那我们要拿到应用描述的信息data,那我们可以通过这种方式拿到,即我们BaseHolder有一个获取数据的方法getData,getData中有个des,
即: view.setText(getData().des);
同时再给这个view设置最大行数7,即: view.setMaxLines(7);
为了模拟的更像一些,把文字的大小也给它设置上去,我们在layout_detail_desinfo.xml的id为tv_detail_des的TextView中看到,它的textSize为14sp 所以在这里设置文字大小为:
view.setTextSize(TypedValue.COMPLEX_UNIT_SP,14);
这里需要一个单位unit,单位的话,这个类叫什么来着,可以点击setTextSize代码进入源码查看,查看到这个类叫TypedValue,然后从TypedValue中拿到这个sp的单位,即TypedValue.COMPLEX_UNIT_SP
size的话 我们是14sp 这是他的大小
模拟的更像一点,这样的话模拟出来的高度才完全一致
这时候再对这个TextView测量,看他到底有多宽,多高,知道多宽多高之后,就可以把这个值返回回去
我们view有个方法叫measure,它可以传两个参数,
一个widthMeasureSpec,一个heightMeasureSpec,这两个参数我们以前传的是0,就去测量了,
在这个地方就不能传0了,因为我们这个TextView是纯用代码去打造的TextView,它本身就没有它的宽高的一些模式啊属性啊之类的特点,之前之所以可以传0,是因为之前我们的那些布局都是写在布局文件中的
布局文件里面它就matchParent,wrapcontent它的宽高写死,布局文件中写好了,只不过人家还没有来得及从布局文件中加载,然后去测量,所以传个0之后,就催促他去根据布局文件中去设置,然后把它弄出来,但现在在这个地方,没有布局文件,TextView到底该怎么展现,matchParent的呢还是wrapContent呢,它的宽高的规则,展示的模式,我们都不能够确定,这时候我们就需要把这个两个值给它了,而这两个值我们怎么去给它呢 这两个值我们要完全和我们"应用描述信息"的TextView的宽高模式要完全的匹配,这样的话,才能够保证他模拟的够像,这时候的话 我们就去获取下"应用描述信息"的TextView的宽高的一个规则,这个规则怎么拿到呢
首先我先拿到TextView对象,就是我们的tvDetailDes,通过它可以拿到它的一个测量后的宽度,
即: int measuredWidth = tvDetailDes.getMeasuredWidth();
然后我们定义一下widthMeasureSpec,应该等于MeasureSpec.makeMeasureSpec(size,mode);
这时候这个size,就是我们tvDetailDes的测量后的宽度measuredWidth,而mode模式的话,tvDetailDes的这个宽度是填充父窗体的,即确定的,所以mode就为MeasureSpec.EXACTLY
即:int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY);
同时也可以拿到高度的值,我们“应用描述信息”的TextView的高度它是不确定的
就说有多高就是多高,它的内容越多就越高,越少就越低,所以它目前的宽高模式,我们就叫做ATMOST
是一个至多的模式,至多模式一般对应的是我们的WrapContent,其实就是包裹内容,即高度由文本内容多少决定,而它size到底应该是多少呢,这个size不确定,但是我们可以给他定义一个最大值,即看“应用描述信息”再高能高到多少,最大值的话你可以写这个屏幕的高度也行,即最大它也不可能超过屏幕的高度,现在我们的屏幕分辨率是480*800,我们这个size高度写个2000差不多
最后我们再去测量,即:
view.measure(widthMeasureSpec, heightMeasureSpec);
测量完了之后把测量之后的结果getMeasuredHeight()返回回去,即:
return view.getMeasureHeight();
这个就是我们获取7行textview的高度的逻辑方法
这块内容可能有些同学稍稍有点晕,其实用的不是特别多,除非以后工作都是在写自定义控件,各种动画
但是其实80%的情况下没那么复杂,真的涉及到非常牛的动画的话,也是用开源的框架,现成的自定义控件,抄过来直接去用
我们还得写个方法去获取完整的较长的高度,即mLongHeight,这时候同样是在DetailDesInfoHolder.java中去写个方法getLongHeight,和获取7行textview的高度方法getShortHeight比较类似,把它复制一下,改名为getLongHeight,
唯一的区别是把view.setMaxLines(7);删掉,即有多少行就显示多少行
这两个方法写完之后,怎么去用呢,就说在一开始的时候,初始状态展示7行高度,
即在refreshview方法中 先把我们的高度分别拿到手,并赋值给mShortHeight和mLongHeight 即:
mShortHeight = getShortHeight(); mLongHeight = getLongHeight();
初始状态要展示7行高度的话,那我们就给他布局参数去修改下高度,即
mParams.height = mShortHeight;
同时的话,我们把它设置给TextView,即:
tvDetailDes.setLayoutParams(mParams);
再把这个东西稍微总结一下:
这个逻辑其实和昨天那个安全有点像,就说他也是动画,但是安全那个不是从0开始而是从一个7行的TextView开始,这时候呢,我们写了两个方法,一个是getShortHeight方法去获取7行的TextView高度,给它设置最大行数为7,它就只展现7行,同时把完全的高度getLongHeight方法
也拿到手,这时候呢 我们按键点击的时候,即toggle方法中 ofInt从mLongHeight到mShortHeight
从mShortHeight到mLongHeight都初始化好了,而在addUpdateListener动画去更新的时候呢
拿到它当前最新的一个高度,把它赋值给布局参数,然后把它设置给tvDetailDes
而在刚开始进来的时候,即在refreshView方法中,我们就让他显示7行的高度,所以我们把短的布局
赋值给mParams,同时给它去设置参数
运行程序调试,点击“应用描述信息”展开的时候,会展开很高的距离,即描述信息展示完了之后,还会展示大片的空白
什么原因呢,我们高度好像计算的有点问题,这个shortHeight没啥问题,longHeight太长了,看向getLongHeight方法也没有问题
最后发现,原因是在refreshView方法中 这个布局还没有完全绘制出来,所以它不能确定到底应该是多高
所以它把最大值2000像素返回来了,所以在布局绘制完了之后,即在toggle方法的时候,肯定已经绘制完了吧,因为绘制完了之后,才有可能去点击,点击之后才会去掉这个toggle方法, 所以别在refreshView中写mLongHeight了,在toggle中写,为了保险,我们把mShortHeight也在toggle中去再次加载一下,即在toggle方法中写如下:
mShortHeight = getShortHeight(); mLongHeight = getLongHeight();
这时候在运行程序,效果刚好
那这时候有一个小细节,比如说我们现在某段“应用描述信息”的文字不够7行的时候拿到ShortHeight,
它设置的是固定的7行的高度,拿到LongHeight的时候,展现完整的高度,但是他本身就不够7行,这时候是不是就可能LongHeight小于ShortHeight,而当LongHeight小于ShortHeight的时候,你们觉得还有必要点击它展开吗 根本就没法展开,而且它可能会往回缩,所以如果它不够7行的话,我们肯定是不去给它点击展开 所以在toggle方法中初始化动画的时候,要判断是否够7行,如果够7行,才有展开和收起的动画,即只有mLongHeight > mShortHeight的时候才有必要去初始化这个动画器,等于的时候也没有必要扩展,else中也是一样的,只有大于的时候才去初始化动画器
那这时候就会报一个问题,就是动画器它压根就可能没有初始化,所以默认给它设置成null
然后如果是没有初始化是null的话,下边animator.addUpdataListener就会报空指针错误
所以在这还要加个if判断,当animator !=null时才去对他进行一个动画的启动
还有一个细节,当我们点击展开或关闭的时候,箭头的方向也要有个变化,
所以动画结束之后要更新箭头的方向,给animator添加监听,在监听方法中的onAnimationEnd方法(动画结束之后走到这个方法中来)中更新箭头的方向
要更新箭头方向,首先要拿到箭头对象,箭头对象时一个ImageView,我们已经把他findViewById拿到手了,即ivArrow
这时候要判断,如果isOpen,即如果是展开的情况下,箭头应该是向上的
else反之
运行程序 效果刚好
现在还有一个很变扭的问题,点击之后展开时展开了,但是我还得往下滑才能看见后边的内容,
能否点击后,展开的时候,能否让他自动的滑到最底部呢,那就是让ScrollView自动滑到最底部,
现在怎么才能拿到ScrollView对象呢,有些同学说你在initView方法中findViewById不就可以拿到吗
拿不到,findViewById我现在加载的是layout_detail_desinfo.xml这个布局,而这个布局的话,它是一个线性布局,它只是一个人家ScrollView里边的小小的一个模块,真正的ScrollView在我们的
HomeDetailActivity中加载的layout_home_detail.xml布局最外层,
所以在DetailInfoHolder.java的initView方法中根本拿不到这个ScrollView
有些同学说 那能不能把ScrollView对象传过去呢
在HomeDetailActivity的onCreateSuccessView方法中findViewById拿到ScrollView对象之后呢
然后就把它传给我们的DetailDesInfoHolder.java 传过来不就有ScrollView对象了吗?
我们当然可以传 但是这里再简绍大家一个另外的方式,也是可以拿到ScrollView的,
即:专门写一个方法getScrollView去获取ScrollView对象,怎样去拿到ScrollView对象呢,想想哈
ScrollView它本身肯定是“应用描述信息”模块布局的它的爹,也有可能是它的爷爷,反正肯定就在它上层的某一个布局里边,有个ScrollView,那这时候呢 我就向上一层一层的去找,去看哪个到底是ScrollView,如果是的话那就拿来用, 怎么去看呢?
我们先拿到它的一个根布局,其实随随便便拿个布局都行,因为随随便便一个 反正都是往上找,随便拿个布局tvDetailDes,我们通过它去getParent,拿到他的爹,按ctrl+Q,返回一个ViewParent类型的parent,那这个就是它的一个爹,
即: ViewParent parent = tvDetailDes.getParent();
它这个爹也是一个View,所以把类型ViewParent改为View,但是会报错,看了ViewParent源码,原来他是一个接口,所以不能把ViewParent改为View
然后while循环判断parent是否instanceof ScrollView,即它是不是ScrollView对象,如果它不是, 我们的parent要继续往上找,即parent = parent.getParent();直到找到它是的时候,这个while循环才结束了
如果它是,那这个Parent就是我们要找的ScrollView对象,就可以return parent;那这时候就可以确定它是ScrollView了,所以把返回值void改为ScrollView 当然这时候返回的parent就可以强转为ScrollView
这是我们获取ScrollView的一种方式,这个方法只有保证你这个布局中确切是有ScrollView的情况下,才有去用,否则就很有可能造成死循环
getScrollView方法写完之后,当动画结束之后,即我们到animator.addListener监听处的onAnimationEnd方法中,让ScrollView自动滑动到最底部
首先我们先拿到ScrollView对象,即getScrollView 然后按ctrl+Q,返回一个ScrollView类型的scrollView
然后scrollView有个方法fullScroll,有些方法实在记不住,你就网上查,比如你就百度scrollView滑动到最底部的api 就可以搜出来这个fullScroll方法 有些同学关键字他不会写,多搜几次你就慢慢的会写关键字了, 我们还是百度关键字,点进去具体看下它是怎么去调用,它怎么去调用的呢,第一种方法:它是用Handler post了一个Runnable对象,在Runnable对象里边呢,它去让它滑动即scroll.scrollTo(0,offset);它说第一种实现比较麻烦,推荐用第二种叫fullScroll,而这个fullScroll它也是post了一个Runnable对象,在Runnable对象中,让它去跳到最下边,
即:scrollView.fullScroll(ScrollView.FOCUS_DOWN);
这时候有些同学会问,为什么还要post一个Runnable对象,这个和你们刚学android的人不太好解释,
我之间你用fullScroll直接调用它也没事,但是有时候他就是会出现一些小问题,就说因为它在布局在画的时候,它有时候代码顺序,手机运行速度会导致他这个布局不起作用,这时候通过几年的发现,发现有些人建议你post一个Runnable对象来调用它这个方法,它就能够保证他比较稳定,所以大家普遍用post方法,其实你不用post方法也是ok的,但个别情况下就会出现一些问题,所以的话才post一个Runnable
那需不需要一个Handler去post runnable对象呢,也不需要,因为view它本身就有post runnable对象的这样一个方法 随便找一个view,我就找ScrollView,它就有post runnable对象这样一个方法
而这个肯定是在主线程的 post它是在主线程的
以上就是我们应用介绍这样一个模块
这个模块比较复杂的一点就在于 动画的这个最短的高度,和最长的高度的确定,当时我们是模拟了一个TextView去确定它的高度
然后就是他ScrollView的这样一个获取,通过一层一层往上找,找到ScrollView之后,再去调ScrollView这样一个fullScroll的方法,让它自动跑到最底部
DetailDesInfoHolder.java
/**
-
应用详情页-应用描述
-
@author Brant
*
*/
public class DetailDesInfoHolder extends BaseHolder {private TextView tvDetailDes; private RelativeLayout rlDetailToggle; private TextView tvDetailAuthor; private ImageView ivArrow; private int mShortHeight; //7行较短高度 private int mLongHeight; //完整的较长高度 private boolean isOpen = false ; //记录当前的状态是展开还是收起 默认收起 private LayoutParams mParams; @Override public View initView() { View view = UIUtils.inflate(R.layout.layout_detail_desinfo); tvDetailDes = (TextView) view.findViewById(R.id.tv_detail_des); rlDetailToggle = (RelativeLayout) view.findViewById(R.id.rl_detail_toggle); tvDetailAuthor = (TextView) view.findViewById(R.id.tv_detail_author); ivArrow = (ImageView) view.findViewById(R.id.iv_arrow); rlDetailToggle.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { toggle(); } }); return view; } @Override public void refreshView(AppInfo data) { tvDetailDes.setText(data.des); tvDetailAuthor.setText(data.author); //初始化状态展示7行高度 mShortHeight = getShortHeight(); //mLongHeight = getLongHeight(); //初始状态要展示7行高度的话,那就给他布局参数去修改下高度 mParams = tvDetailDes.getLayoutParams(); mParams.height = mShortHeight; //把参数设置给TextView tvDetailDes.setLayoutParams(mParams); } /**
-
展开或收起应用描述信息
*/
private void toggle(){mShortHeight = getShortHeight(); mLongHeight = getLongHeight(); ValueAnimator animator = null; if (isOpen) { //收起 isOpen = false; if (mLongHeight > mShortHeight) { animator = ValueAnimator.ofInt(mLongHeight,mShortHeight); } }else{ //展开 isOpen = true; if (mLongHeight > mShortHeight) { animator = ValueAnimator.ofInt(mShortHeight,mLongHeight); } } if (animator !=null) { animator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator arg0) { Integer height = (Integer) arg0.getAnimatedValue(); mParams.height = height; tvDetailDes.setLayoutParams(mParams); } }); animator.addListener(new AnimatorListener() { @Override public void onAnimationStart(Animator arg0) { } @Override public void onAnimationRepeat(Animator arg0) { } @Override public void onAnimationEnd(Animator arg0) { //更新箭头方向 if (isOpen) { ivArrow.setImageResource(R.drawable.arrow_up); }else{ ivArrow.setImageResource(R.drawable.arrow_down); } //让ScrollView页面自动滑动到最底端 final ScrollView scrollView = getScrollView(); scrollView.post(new Runnable() { @Override public void run() { scrollView.fullScroll(ScrollView.FOCUS_DOWN); } }); } @Override public void onAnimationCancel(Animator arg0) { } }); //设置动画时间 animator.setDuration(200); animator.start(); } } /**
-
获取7行TextView的高度
* -
@return
*/
private int getShortHeight(){
// 模拟一个TextView
TextView view = new TextView(UIUtils.getContext());
view.setText(getData().des);
view.setMaxLines(7);// 最多展示7行
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);//初始化宽高模式,必须和布局layout_detail_desinfo.xml的id为tv_detail_des的TextView保持完全一致 int measuredWidth = tvDetailDes.getMeasuredWidth(); // 宽度填充屏幕,已经确定, 所以是EXACTLY int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY); //高度不确定,由文本内容多少决定,相当于wrap_content,在此使用至多模式AT_MOST,这里的2000是高度最大值, 也可以设置为屏幕高度 int heightMeasureSpec = MeasureSpec.makeMeasureSpec(2000, MeasureSpec.AT_MOST); //最后再去测量,即tvDetailDes得到的规则要作用在模拟的textView上,保持其高度一致 view.measure(widthMeasureSpec, heightMeasureSpec); // 返回测量的高度 return view.getMeasuredHeight(); } /**
-
获取完整TextView的高度
* -
@return
*/
private int getLongHeight(){TextView view = new TextView(UIUtils.getContext()); view.setText(getData().des); view.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); //初始化宽高模式,必须和布局layout_detail_desinfo.xml的id为tv_detail_des的TextView保持完全一致 int measuredWidth = tvDetailDes.getMeasuredWidth(); // 宽度填充屏幕,已经确定, 所以是EXACTLY int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY); //高度不确定,由文本内容多少决定,相当于wrap_content,在此使用至多模式AT_MOST,这里的2000是高度最大值, 也可以设置为屏幕高度 int heightMeasureSpec = MeasureSpec.makeMeasureSpec(2000, MeasureSpec.AT_MOST); //最后再去测量,即tvDetailDes得到的规则要作用在模拟的textView上,保持其高度一致 view.measure(widthMeasureSpec, heightMeasureSpec); // 返回测量的高度 return view.getMeasuredHeight(); } /**
-
从子控件一层一层向上找,直到找到ScrollView对象为止(注意:一定要保证布局中含有ScrollView,否则会死循环)
-
@return
*/
private ScrollView getScrollView(){
ViewParent parent = tvDetailDes.getParent();
while (!(parent instanceof ScrollView)) {
parent = parent.getParent();
}
return (ScrollView) parent;
}
}
Day05 05.自定义排行模块控件原理分析 ##
Day05 06.自定义排行模块开发01 ##
Day05 07.自定义排行模块开发02 ##
排行模块自定义View ##
我们接下来再搞一个自定义控件,纯手工打造一个排行的这样一个自定义控件,这个玩意确实比较坑爹,
先分析排行的自定义控件:
你会发现排行模块中是一个又一个的小孩子,都是TextView,我们把它都添加进来,我们当时怎么去用到它的呢,看下当时用的一个方式 来到我们当时的HotFragment中来,当时是怎么做的呢,当时是new了一个 FlowLayout 搞一个for循环 有多少孩子我们就创建多少个TextView,在TextView中我们再去给他设置颜色,背景,状态选择器,点击事件等等都给他设置进来, 就是我们给这样一个FlowLayout添加TextView,当然你要添加imageView也没有问题,那我们现在分析下这个FlowLayout,你看起来它是挺简单的,但是它还是挺复制的,我们去分析下,要开发这样一个东西,具体要怎么去做
画个图我们去看下:我们要在手机上画一个又一个的小格子,我先画一个小格子,这个小格子有长有短,具体由我们的文字决定的,文字越长,这个TextView是不是就越长,即宽度都不一样,它有一个水平的间距,隔一段水平间距,它又是一个小格子,这是他在正常情况下的一个情况,那么我们现在要分这么几种情况,去分别去分析一下,这个格子到底应该怎么去画:
1.一行的控件刚好填满,但再增加一个水平间距的话,就超出了屏幕范围,这种情况下要换行
2.如果在当前行的基础上,再增加一个控件就会超出这个屏幕范围,此时要换行,在新的一行添加我们的控件
3.如果当前行没有控件,而且要增加的控件宽度本身就已经超出了屏幕宽度范围,此时也要强制添加到当前行,然后换行
4.如果当前行中的控件没有把行填满,要将当前行剩余空间平均分配给当前行每个控件,让每个控件比原来更宽一些 以便每一行宽度能对齐
5.如果当前行控件高度不一致(以高的控件的高度为准则),矮的控件要居中显示
6.水平和竖直方向的间距
7.设置最大行数
接下来就可以手动打造排行页自定义控件,在com.bq.googleplay.ui.widget包下新建一个类
MyFlowLayout,那这个自定义控件,它里面可以添加一个一个的孩子,所以让他继承自ViewGroup
那这个ViewGroup的话,会重写这么几个方法,onLayout是设置布局位置的方法,实际上它还有一个方法,
onMeasure这样的一个方法来测量布局的宽高,而我们就应该着重测量布局宽高里面去做文章
我们测量布局宽高怎么去做呢 我们首先可以拿到这样的一个宽度,
比如说拿到一个 int widthSize,这是我们控件的宽度,控件的宽度怎么拿到的呢
我们拿到MeasureSpec,我首先得拿到我控件的总宽度是多少,它getSize(measureSpec)
就可以从onMeasure方法的参数widhtMeasureSpec中把这个控件的宽度拿到手,我们所拿到的宽度是什么宽度呢
就是排行页面整体是一个控件,拿到的宽度就是他整体整个的这样一个宽度,那实际上我们在填充
控件的时候,从哪里才开始填充控件的呢,就是从第一行开始填充的,也就是说他这个控件有可能会有左右的边距,而这个边距的话,内边距我们是无法去填充我们的控件,所以的话,我们需要把边距的宽度给剔除掉,才是有效的控件整体的一个宽度,所以在MeasureSpec.getSize(widthMeasureSpec)处应该减去getPaddingLeft(),再减去getPaddingRight(),这是我们的有效宽度,
我们再来一个有效的高度,即:
int heightSize = MeasureSpec.getSize(heightMeasureSpec) -getPaddingTop() -getPaddingBottom();
同时我们也可以拿到他的宽高模式,并分别叫做widthMode,heightMode
这只是我们整体的这样一个控件的特点,接下来就要分析下每个孩子,它具体多宽多高呢,我们根据孩子的宽高,到时候要统计要不要换行, 我们这时候对这个孩子进行一个遍历,getChildCount()拿到他孩子的数量
我们搞一个for循环去遍历所有的孩子,遍历所有的孩子我们要做什么事情呢,我们要拿到孩子的一个宽度和高度的一些信息,获取孩子的子控件的宽高信息,这时候我们怎么去做呢,首先我们先拿到子控件对象
我们先去getChildAt(i)拿到子控件的对象childView,childView有个measure方法,
它里面需要传个widthMeasureSpec和heightMeasureSpec,这些控件我们都是动态用代码去new进去的
没有布局文件,所以的话,你还得把这个widthMeasureSpec和heightMeasureSpec都给他拼出来,
即int childWidhtMeasureSpec等于什么呢 等于MeasureSpec.makeMeasureSpec(size,mode)
首先传宽度,那宽度的话 我们把有效的这个宽度值给他传进来,这是屏幕的一个宽度,它最大的一个宽度
就是这么大,而这个模式mode到底是什么模式呢,我们得判断,判断一下原始的父控件的模式
是否等于MeasureSpec.EXACTLY,即widthMode == MeasureSpec.EXACTLY
这个就指的,如果父控件的宽度的值是一个确定的话即EXACTLY,这个时候我的模式就是多少呢 我的模式就是AT_MOST,否则的话我的模式就是父控件的模式widthMode
即: 如果父控件宽度是确定的值,那么孩子宽度模式就是至多模式,怎么去理解,
就说我们现在父控件的宽度就是确定的,填充屏幕,他就是一个确定的值,而里面的孩子的值,
它的宽度还真的不确定,就说他的文字长了他就长,文字短了就短,所以说他是一个包裹内容的模式
那包裹内容的模式,对应过来其实就是一个AT_MOST模式,至多,它有多少就是多少,
我们另外一种就是说,剩下的情况,子控件模式和父控件模式相同,也就是说父控件如果不是确定的话
那么父控件是多少,子控件就是多少,
这个又怎么去理解呢,就说父控件它现在不是确定的话,它有哪几种情况呢,它有两种情况,一种是AT_MOST至多模式,至多模式他就是说父控件它就是包裹内容的模式,父控件都包裹内容了,父控件都多大就多大,
那孩子是不是也当然是包裹内容,还有一种情况就是说父控件是UNSPECTED,即未确定的模式,
父控件都未确定那孩子是不是也不确定,所以这就是另外两种情况,而我们目前父控件基本是确定的模式EXACTLY
,则子控件是AT_MOST
上边是一个宽度的模式,那我们再来一个高度的模式,高度的模式的话,
就叫int childHeightMeasureSpec,它等于什么呢 还是等于这个MeasureSpec.makeMeaureSpec(size,mode);
size等于什么,还是等于heightSize,mode还是和宽度的mode一样,抄过来然后把最后三目运算的widthMode改为heightMode
那这时候我们拿到孩子的宽度模式和高度模式之后的话,我们就可以去测量它的尺寸,去确定它的这个尺寸了
即:childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
测量它的尺寸之后,childView就可以getMeasureWidth();拿到孩子的宽度了,就叫做int childWidth
拿到子控件的宽度之后,要根据这个宽度来决定它有没有超出这一行,因为一个宽度加一个宽度再加一个宽度,我们会超出一行,所以要申明一个全局的变量来记录一下我们当前行的目前的宽度是多少,即:
private int mUseWidth; //当前行已经使用的宽度值
那这时候我们拿到孩子的宽度之后,就给当前使用的宽度等于加等于childWidth,
即: mUsedWith += childWidth; //已使用宽度增加
增加完之后,看这个高度有没有超出我们的屏幕的宽度,因为有时候你可能原来人家已使用的就这么宽了,你再增加,就唰的超出去了,所以的话,在这个地方要判断有没有超出,即mUsedWidth是否还小于我们有效的宽度
如果还小于的,那就说明当前行仍可添加子控件,这是一种情况,在搞一个else回头再去处理
当前行仍可添加子控件,那我们就要把这个子控件,添加给这个当前行,我们这个控件现在是一行一行的
所以说这个一行,我们完全可以用一个对象,把它封装起来,我们写一个class Line{},来封装每一行的这样一个对象,他里面应该有什么方法呢,我们应该有个方法叫做addView的方法,即添加一个子元素,因为一行里面,我们是不是可以给这一行里面添加一个一个的item,所以应该给他写个添加一个子元素的方法addView
添加子元素添加给谁呢,我们搞一个集合来去添加,即:
private ArrayList<View> mChildList = new ArrayList<View>();
add的话就是给这个mChildList去add,所以在addView方法中添加参数(View childview)传过来,
然后我们就给这个add一个View,即添加一个子元素
你给一行添加了一个元素之后呢,那你这一行本身它的宽度就会变得大一些,所以的话,我们再申明一个他的总宽度 即:private int mTotalWidth; //当前行的总宽度
那在这个时候呢,当前行的总宽度加等于一个我们子控件的宽度,而子控件的宽度的话,
我们通过childView.getMeasureWidth()拿到他的一个宽度,而且同时的话,我们并不是说只加一个宽度,
因为我们还要加一个水平边距,要加进来的话,那我们还得搞一个水平的间距,即申明一个水平的间距,我们设置6个dp 即:
private int mHorizontalSpacing = UIUtils.dip2px(6);//水平间距
顺便把竖直间距也写上吧,我们再写一个竖直间距,即:
private int mVerticalSpacing = UIUtils.dip2px(8);//竖直间距
那在mTotalWidth += childView.getMeasureWidth()这再加一个水平的间距
即:mTotalWidth += childView.getMeasuredWidth() + mHorizontalSpacing;
这就是当前行的总宽度,那还有高度,高度应该是多少呢,我们当时分析了一下,
有时候有可能会是这种情况,即某个控件特别高,某个控件又比较矮,这时候最终是以谁为准呢
我们以高的那个为准,所以的话,在这个地方,我们再去写一个变量,叫做mMaxHeight
即:private int mMaxHeight;//当前行的最高的控件的高度
这个最高控件的高度,我们怎么去定义呢,我们这样去定义,
首先我先通过这个childView去getMeasuredHeight()拿到它的高度值为 int measureHeight
然后我们拿这个高度值和mMaxHeight进行一个比对
看mMaxHeight是否小于measuredHeight,即你的这个高度是否小于我现在测量的这个新的高度
如果小于的话,应该等于人家,因为我永远要找最大的那个高度值, 否则的话,还要维持原状
这是设置当前行最高控件的值,这个就是我们在确定它的宽度和这样一个高度的方法
同时我们再暴露这样一个方法,getChildCount,拿到我们孩子的个数,并返回,即
return mChildList.size();
Line对象的这样一个封装,我们现在已经封装好了
那这个line对象,我们到时候怎么去初始化呢,当然是一开始就得初始化一行
然后再往这一行中添加一个一个的元素,所以的话,我们在遍历所有的孩子的for循环还没有开始之前,
去初始化我们的行对象,怎么去初始化呢,我们在成员变量处去申明一个行对象,
即: private Line mLine; //当前行的对象
这是我们一行的这样一个对象
怎么去初始化呢 在遍历所有的孩子的for循环上边,做一个判断,判断它是否为空,判断如果当前行等于空
这时候呢就让mLine等于new一个Line(); 即: mLine = new Line();
if (mLine == null) {
mLine = new Line();
}
行现在就初始化好了,接下来回到if(mUsedWidth < widthSize),当我发现我的这个已使用的宽度
现在还小于我们有效的宽度的话,这时候就可以在当前行中继续添加我的子控件,这时候mLine就可以调它的
addView方法,将我们的childView添加到我们的控件中来,而同时你添加完之后,每个控件都有它的水平间距
你是不是得把水平间距也加进来,所以在mUsedWidth基础上加等于mHorizontalSpcaing,增加水平间距
这时候就有可能出问题了,因为你加了水平间距之后可能刚刚好达到widhSize高度,你再一加水平间距的话
有可能就超出这个边界了吧,所以我们判断,是否增加水平间距之后,是否超出了最大高度
即 if (mUsedWidth > widthSize ) { 大于的话换行,换行的话,我们写一个方法newLine,单独去换行
你要换行的话,你得先把上一行的内容先保存下来,因为我们是一行又一行,它实际是行的集合,
所以我们应该把上一行保留下来,所以我们先搞一个集合,即:
private ArrayList<Line> mLineList = new ArrayList<MyFlowLayout.Line>();//所有行的集合
在newLine方法中,我们要保存上一行对象,那怎么去保存呢,就让这个所有行的集合去add一个把上一行的mLine保存进去,即: mLineList.add(mLine); 就是我们的mLine吧,即我们在当前行仍可添加子控件处给这个mLine去addView,即mLine.addView(childView); 最后要把这个mLine保存到上一行中来
而我们这个控件中还有一个最大行数的限制,就说超出这个限制之后,我们就不容许再去添加新的行对象了
所以的话,我们在这个地方,也要进行一个限制了, 即我们再写一个成员变量,这是他的最大的行数,我们写个100;即: private int MAX_LINE = 100; //最大行数为100
就是说你超出100之后,我们就不在添加新的行了,
这时候我们再回到newLine方法中 进行一个判断,判断现在的集合的大小,是否小于我们的MAX_LINE,如果小于的话,还可以继续添加,这时候我们再初始化一个新的对象mLine,那既然是一个新的行对象的话,那我们已使用的
宽度肯定是0吧,因为现在行对象中,一个控件都没有,所以我要把这个mUsedWidth等于0;即
mUsedWidth = 0;//已使用行的宽度清零
然后返回一个true;表示我们创建行对象成功。那这时候就可以把返回值void改为boolean
在什么情况下返回false呢,现在的集合的大小大于我们的MAX_LINE,就返回false,表示你不能再去添加行对象了吧
那这个时候再回到换行的位置,我们发现他加了一个水平间距之后,已经超出了宽度,那这时候就可以去
换行,调一个newLine() 返回值为boolean newLine,那你需要判断这个newLine是true还是false,即有没有必要继续添加,如果newLine等于false的话,你是不是不应该继续添加了吧,这就是创建行失败,这时候应该跳出循环,不再添加子控件,即这个地方应该break;
我们简化一下,没有必要在这申明一个变量 即 boolean newLine = newLine();
而是直接将newLine()放在if判断处即 if(!newLine())
这就是换行吧,这个换行肯定调呢,调完之后如果是false的话,那就失败,它就break了吧,这是我们的第一种情况
我们接下来再看,如果mUsedWidth已经大于等于我们的widthSize,就已经超出我们现在的宽度了,那这时候就在else处去判断一个东西,在else中判断我们当前行里面getChildCount,是否等于0,即判断当前行中是否还未添加任何的一个控件,这时候有些同学可能会疑惑,都没添加过控件,宽度怎么可能还会超出到这个地方来呢
是因为childwidth这一个控件就已经够宽了 本来他是0 这个控件以增加就马上就超了 所以在这种情况下,
也就是我们的哪种情况呢,就说“如果当前行没有控件,而且要增加的控件宽度本身就已经超出了屏幕宽度范围,此时也要强制添加到当前行,然后换行”
即 如果它当前行等于0,强制将他添加进来 这时候是我们的mLine,强制add一个childView
但是添加进来后,宽度已经超出了界限,那这时候这一行是不是只能显示这一个控件啊,那只能显示这一个控件,那你下一行必须要换行了吧,所以在这里也要换一下行,我们还有一个else的情况,就说当前的控件mUsedWidth
就说他本来就有控件呢,就是说的我们这种情况,即,已经有两个控件了,这时候你在添加的时候,它就超出去了吧 超了之后你是不是得先换行然后在添加啊 所以else中先换行,完了之后,我们在给这个mLine再去addView
把这个childView添加进来。
同时添加进来之后呢,mUsedWidth现在宽度就变了,让他加等于childWidth,同时还要加我们的一个水平间距mHorizontalSpacing, 即:
mUsedWidth = mUsedWidth + childWidth +mHorizontalSpacing;
这种情况就是我们的第二种情况,即:2.如果在当前行的基础上,再增加一个控件就会超出这个屏幕范围,此时要换行,在新的一行添加我们的控件
这就是我们遍历所有的孩子的这样一个for循环,通过这个for循环我们就能够一行一行去遍历,全都添加进来
这个for循环结束之后,我们在做一个判断,我们要把最后一行也要添加这个对象里面来,
即:如果发现这个mLine不等于空,并且我们这个集合中!mLineList.cantains一个mLine,
即不包含我们当前的这一行,再加一个判断,并且mLine.getChildCount()>0
这时候我们把mLine强制添加到mLineList集合中
为什么要加这样一个判断呢,就是说我们for循环到最后一次的时候,比如说已经到了最后一行了之后,那我们去添加控件的时候,有可能造成我们最后一行的那个对象,没有添加在我们的集合里边来,我们在什么时候把这个对象添加到集合中的呢,是不是在newLine换行的时候,把这个mLine才添加到我们的集合中来的吧,但如果在最后一行我没有调newLine换行这个方法的话,因为我觉得我已经是最后一行了我刚刚好能展现出来不需要换行,那这个时候就没有调newLine方法,没有调newLine方法的话,是不是就把上一行那个对象,忘了保存,所以在这个地方要做一个判断,判断我们mLine不为空,而且集合中不包括这个mLine,并且这个mLine还大于0 大于0这个地方才有意义,然后满足这些条件之后,才将他追加到我们的mLineList集合中来
这个控件到这里,我们完成了50%左右
今天上午主要是在写这个排行的自定义控件的模块,大概分析了下它的各种情况下,应该怎么去按照他的宽度
进行换行计算,写了一个MyFlowLayout布局 让它继承ViewGroup,继承这个ViewGroup之后,重写它的onMeasure方法进行一个测量,测量的话我们可以拿到他的有效宽度,有效高度,宽度模式,高度模式,
这时候就开始对每个孩子进行遍历,分别拿到每一个孩子具体的宽高是多少,那要拿到孩子的两个测量值的话,还是要传两个值,一个是它宽度模式的这样一个值,一个是它高度模式的这样一个值,等for循环循环完之后,我们就给每个行都添加了孩子对象,而且每一行我们都用集合ArrayList<Line>维护了起来,
等for循环遍历完了,有可能造成最后一行的这样一个行对象没有添加到集合中来,所以最好在for循环之后判断并添加
现在开始我们下午的内容,接下来我们要根据当前行数,来确定我们整个布局的宽高,宽度好办,就是他填充屏幕
所以int width 等于MeasureSpec.getSize(widthMeasureSpec);拿到我们的widthMeasureSpec,这就是拿到他的一个宽度,这就是他确确实实的一个宽度,就和原来的是一样的,然后再拿到控件的高度,int height=0,这个height就不太好确定了,因为height现在是怎么去确定的呢,有多少行,而每一行高度是多少,把它们加在一起,才是我们的总的高度,所以搞一个for循环,对每一行进行一个遍历,这时候就可以拿到他某一行的对象,通过mLineList.get(i)拿到这一行的对象line,而这一行它的高度是由他最高的子控件来确定的,即让height 加等于line.mMaxHeight; 等这个for循环完之后,这个height才会越来越大,越来越大,这是就把所有的行数都遍历下来了,最后我们还要把竖直的间距都加进来,我们目前计算的是每一行的情况,但是我们也要把间距搞进来吧,
要看有多少竖直间距,就要看你有多少行,比如现在只有三行,那就只有两个竖直间距,4行是3个,所以应该是行数-1,那这时候在height的基础上加等于一个竖直间距mVerticalSpacing *(mLineList.size()-1);
把整个布局的宽高度width,height计算出来后,把它们设置给我们的控件,
有一行代码 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
它就是拿它原始的宽高去底层测量,那他怎么测量的呢,点击super.onMeasure的这个onMeasure进去源码,它调的是setMeasuredDimension,把这个宽高测量进去,而我们这个时候不在用它父类的这个宽高了,我们把super这一行注释掉,自己去写,自己setMeasuredDimension(measuredWidth,measureHeight),这里把自己测量的width和height给它传过去,来自己设置布局父控件宽高,
这些都是在我们的onMeasure方法中,onMeasure方法写完后,现在位置还没给它确定好,在onLayout方法中给它设置位置,所以的话,我们得设置每一个item的位置,所以再来到class Line方法中来,在它里面维护了一堆孩子
我们看他的位置怎么设定,我们再写个方法,叫做layoutView,来设定子控件位置,一般设定位置的时候,都是有上(top)下(bottom)左(left)右(right) 这几个值,这个地方只需要传两个值,一个int left
一个 int top, 因为我们只需要知道它左方和上方的位置就能够把控件的具体位置确定出来了,它的right和bottom不就是left加宽度,top加高度么
那现在怎么去设定子控件位置信息呢,我们可以这样去做,首先我们先分析下我们的7种情况,1,2,3,6,7,我们都处理了,还有4和5我们没有处理,那就放在我们这个layoutView方法中去处理,
4.如果当前行中的控件没有把行填满,要将当前行剩余空间平均分配给当前行每个控件,让每个控件比原来更宽一
些,以便每一行宽度能对齐
那我们怎么知道它有没有填满呢,我们是不是有一个mTotalWidth方法去表示它全部的一个宽度,这时候去判断,如果我们的,我们先写一个变量int surplusSpace = 来表示多余的
我们用完整的宽度减去它目前已经有的宽度,就是剩余的宽度,那它完整的宽度是多少呢,其实就是父控件MyFlowLayout的宽度,它的父控件是多少,这时候要拿的话,就getmeasuredWidth() ,是拿到我们控件的这样一个宽度,返回一个int validWidth,并减去getPaddingLeft()和getPaddingRight(),这就是我们有效的宽度
这时候就用有效的宽度valueWidth减去已使用的宽度mTotalWidth,就是剩余的宽度int surplusSpace,
我们判断剩余的宽度surplusSpace,它是否大于等于0,大于等于0说明有剩余空间,有剩余控件的话,要把它平均分配给每个子控件,那就得知道每个子控件平均能拿多少子空间,surplusSpace除以孩子的数量即getChildCount,就是每个孩子能拿多少空间int splitSpace,这个就是平局分配的结果
将平均分配的结果追加到每一个孩子的控件上,那我们先拿到孩子,对孩子进行一个遍历,for(int i=0;i<getChildCount();i++) 那我们通过mChildList.get(i)拿到他第i个位置的孩子,返回为View childView,然后通过childView去getMeasureWidth,拿到他目前的宽度int measureWidth
那他目前的宽度要增加,即 measuredWidth += splitSpace;//将多出来的宽度分配给子控件
同时也可以通过childView.getMeausuredHiehgt()拿到我们的高度int measuredHeight,
当然高度信息measuredHeight暂时先不用他,因为我们主要是水平方向去平分,高度方向我们不需要平分
有了高度measuredHeight和宽度measuredWidth之后,就可以根据最新的高度和宽度来测量控件了,
即MeasureSpec.makeMeasureSpec(size,mode);首先它的size的宽度就是我们目前追加完平均分配值后的控件宽度measureWidth,而这个时候的mode模式,因为我们已经将他的宽高确定好了,所以为MeasureSpec.EXACTLY
那他返回了一个int widthMeasureSpec,
同理我们再搞一个int heightMeasureSpec,这是我们确定的这样一个高度吧
确定的宽度高度都有了之后,掉一下childView.measure(widthMeasureSpec,heightMeasureSpec),把这两个值传过去,这就是重新测量子控件宽高 这是我们重新分配宽的这样一个逻辑,高不需要分配
接下来再考虑另外一种情况:
5.如果当前行控件高度不一致(以高的控件的高度为准则),矮的控件要居中显示
一般情况下,它的控件怎么去显示呢,比如一行开头有个高的子控件,第二个是个矮的子控件,它默认会左上对齐显示,但是我们现在就要让我们这个矮的控件往下显示,显示到一个居中的范围之内,那就相当于他的top值,原来他默认左上对齐时为0,但是现在居中的话,top值为这么多,这么多就多少呢,是不刚好就我们最大控件的高度,减去
我们本身的高度,再除以2,是不是就是我们这个部分的top值,这个就是我们计算的一个原理
如果某个控件比较矮,要居中显示,到顶端的偏移量int topOffset 就等于我们mMaxheight减去本身的高度measuredHeight,然后再除以2
最后设置我们子控件的具体位置
childView调一下他的layout(l,t,r,b)方法,左、上、右、下,传给他即可 只不过top值需要加个偏移量
right不就等于left加上布局的宽度measureWidth,而bottom值是不是就是top加上topOffset
再加上他布局的高度measuredHeight
topOffset 即:childView.layout(left,top + topOffset,left + measureWidth, top+topOffset+measureHeight);
这时候就有一个问题,你这样去做的话,可能只是把第一个控件已经初始化好了,但是第二个控件的话,top值大家都一样,但是第二个控件的lef的值,他是不是已经发生变化了,所以我们这个left值是不是要在原来的基础上,加上我们刚刚添加上的那个控件的宽度以及水平间距,所以的话,我们要更新left的值,因为我们要往右依次排开显示
所以我们的left的值越来越大越来越大,所以我们这的left的值应该等于left值加上measuredWidth,还得加一个水平间距mHorizontalSpacing,即:left = left + measuredWidth + mHorizontalSpacing;
所以下次再for循环进来到这里的话,left的值就已经变了,它已经接着上一个那个地方继续往右去布置它的控制了
只有这样你才能保证这个控件依次依次排开,当然top值不需要去管,因为都在一行么,就是他的left值会有些变化,那我们刚才分析的是这个屏幕里面剩余的宽度大于等于0的情况,那有没有可能剩余的宽度小于0呢,有可能,就是我们的第三种可能,即屏幕一行的总空间减去布局的空间的时候,它结果肯定是小于0的
layoutView这个方法写完之后,我们在哪个地方调用呢,当然是在我们的onLayout方法中调用的,
这个时候就在onLayout方法中写个for循环遍历所有行,
在所有行mLineList中就可以拿到我们的这个行对象,即 mLineList.get(i)
那这时候呢,掉一下这个line对象的layoutView,传个left值就是l值,top值就是t值,即line.layoutView(l,t);
这是我们遍历所有行对象,分别设定每个行的子控件的位置
具体的位置就在这个layoutView方法中,再去for循环遍历它的每一个孩子,然后把它的每一个位置都设定好了,
在这个地方还有一个小问题,onLayout方法参数top值t,会发生变化的,因为第一行来的时候,top值可能为0,但他第二行的时候呢,这个top值已经变了,它的上一行的高度,再加了一个竖直间距,所以在这个地方对top值进行更新,也就说t应该等于t要加上上一行的高度mMaxHeight,他最大的高度就是他的高度吧,再加竖直间距mVerticalSpacing, 即:
t = t + line.mMaxHeight + mVerticalSpacing;//更新top值
就你更新,他才会一行完了之后top已经变大了,再往下一行,再往下一行,这样的话,依次从上往下,依次排开,那这个就是我们这样一个onLayout方法
那在这里呢,我们这个自定义控件基本上就已经大功告成了,我们这时候就用一下自定义控件,
只有把它写完了才可以运行,因为写不完它运行不起来,怎么去运行呢
找到我们原来的那个HotFragment,原来他里边用的是FlowLayout,我们把它们分别注释掉
我们用我们自己的MyFlowLayout,我们也叫flow,即:
MyFlowLayout flow = new MyFlowLayout(UIUtils.getContext());
这是我们自己的自定义控件,剩下的我们都不用改,直接去运行即可
发现排行的流布局左边都被截断了,这个是因为我们在MyFlowLayout.java中的layoutView方法中,在计算剩余宽度的时候,忘了做一件事情,我们先拿到他的总宽度validWidth,然后用总宽度减去 mTotalWidth,我们目前控件的宽度,然后等于剩余宽度surplusSpace是不对的,减了mTotalWidth之后,我们要将水平间距也剪掉,因为这个间距也不能算到宽度里面,即再减去mHorizontalSpacing*(getChildCount-1),这个就是它一共有这么多水平间距,把它也要剪掉
这才是我们剩余宽度,
这时运行程序,布局变乱了,原因是在onLayout方法中不能直接用参数中left的值l和top的值t
因为这个left的值和top的值,指的是这个控件从一开始的时候从最外层包括paddingTop和paddingleft的地方拿到他left值和top值,实际上是从第一个控件的左上角开始画的,不包括最外层那个PaddingTop和paddingLeft
所以来到onLayout方法中,重新拿一下它的left的值和top的值,即:
int left = getPaddingLeft();
int top = getPaddingTop();
然后再将left值和top的值在line.LayoutView(l,t);中传过来,即:
line.LayoutView(left,top);
运行程序,还是会出现其他问题,这个自定义控件出现问题不好改,因为它也不报错,
原因是在去添加控件的时候,在Class Line 方法中,我们有个变量叫做mTotalWidth,即计算当前行的总宽度,那这个宽度里面,我们当时怎么计算的呢,我们当时是在addView方法中加了个水平间距mHorizontalSpacing,
就是增加这个水平间距捣的鬼,为什么呢,这个地方我们不应该加水平间距,因为我们在layoutView方法中,我们在算总的剩余宽度surplusSpace的时候,把水平间距mHorizontalSpacing减了,所以在这个地方我们就认为这个宽度mTotalWidth,不包含间距的宽度,不然还要-这个水平间距mHorizontalSpacing,不包含这个宽度mTotalWidth,但是我们在addView方法中初始化mTotalWidth时,给它把水平间距mHorizontalSpacing加上了,这个就
是主要原因,当然我们也有一些小细节的原因,但是不会造成那么大的bug,小细节的原因就是上边运行问题的那几个。而下边在计算整个控件高度的时候呢,我们当时是
height += mVerticalSpacing * (mLineList.size() -1) +getPaddingTop()
getPaddingBottom();
没有加这个padingTop和PaddingBottom的,因为它有时候会给这个控件设置padding,我们要把这个padding加上,但实际上我们不加也没有什么问题,因为我们写这个项目的时候没有给我们的FlowLayout去设置任何padding,其实你没设置的话就是0,加上个0不加0都没有什么影响,我们只是为了这个扩展性更强,才加上这个paddingTop和paddingBottom,而刚才出这个bug的原因就是我们在这个width的地方,再去增加的时候,我们就不把水平间距也加上了
到此这个自定义控件就写完了,我们开发中不会去手动写这个东西的,都是找写开源代码,看他有这个效果,就拿来去用,就够了。
MyFlowLayout.java
/**
-
自定义排行页控件
* -
@author Kevin
*/
public class MyFlowLayout extends ViewGroup {private int mUsedWidth;// 当前行已经使用的宽度值
private int mHorizontalSpacing = UIUtils.dip2px(6);// 水平间距
private int mVerticalSpacing = UIUtils.dip2px(8);// 竖直间距
private Line mLine;// 当前行对象
private int MAX_LINE = 100;// 最大行数为100
private ArrayList
mLineList = new ArrayList<MyFlowLayout.Line>();// 所有行的集合 public MyFlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyFlowLayout(Context context) {
super(context);
}
// 设置布局位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingLeft(); int top = getPaddingTop(); // 遍历所有行对象, 分别设定每个行的子控件的位置 for (int i = 0; i < mLineList.size(); i++) { Line line = mLineList.get(i); line.layoutView(left, top); top = top + line.mMaxHeight + mVerticalSpacing;// 更新top值 }
}
// 测量布局宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 有效宽度 int widthSize = MeasureSpec.getSize(widthMeasureSpec)
getPaddingLeft() - getPaddingRight();
// 有效高度
int heightSize = MeasureSpec.getSize(heightMeasureSpec)
getPaddingTop() - getPaddingBottom();
// 宽度模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 高度模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 初始化行对象
if (mLine == null) {
mLine = new Line();
}
// 遍历所有的孩子
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
// 获取子控件的宽高信息
View childView = getChildAt(i);
// 如果父控件宽度是确定的值,那么孩子宽度模式就是至多模式; 剩下的情况,子控件模式和父控件相同
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize,
(widthMode == MeasureSpec.EXACTLY) ? MeasureSpec.AT_MOST
: widthMode);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
heightSize,
(heightMode == MeasureSpec.EXACTLY) ? MeasureSpec.AT_MOST
: heightMode);
// 测量子控件宽高
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
int childWidth = childView.getMeasuredWidth();// 子控件宽度
mUsedWidth += childWidth;// 已使用宽度增加
if (mUsedWidth < widthSize) {
// 当前行仍可添加子控件
mLine.addView(childView);
mUsedWidth += mHorizontalSpacing;// 增加水平间距
// 判断增加水平间距之后,是否超出了最大宽度
if (mUsedWidth > widthSize) {
// 换行
if (!newLine()) {
// 创建行失败, 跳出循环,不再添加子控件
break;
}
}
} else {
// 判断当前行是否还没添加过控件, 强制将当前控件添加到当前行
if (mLine.getChildCount() == 0) {
mLine.addView(childView);// 当前行只添加这一个控件,然后换行
// 换行
if (!newLine()) {
// 创建行失败, 跳出循环,不再添加子控件
break;
}
} else {
// 换行
if (!newLine()) {
// 创建行失败, 跳出循环,不再添加子控件
break;
}
mLine.addView(childView);
mUsedWidth = mUsedWidth + childWidth + mHorizontalSpacing;
}
}
}
// 循环结束之后,有可能导致最后的行对象没有添加到集合中,在此需要添加进去
if (mLine != null && !mLineList.contains(mLine)
&& mLine.getChildCount() > 0) {
mLineList.add(mLine);
}
// 根据当前行数,来确定整个布局的宽高
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = 0;
for (int i = 0; i < mLineList.size(); i++) {
Line line = mLineList.get(i);
height += line.mMaxHeight;
}
height += mVerticalSpacing * (mLineList.size() - 1) + getPaddingTop()
getPaddingBottom();
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(width, height);// 设置父控件宽高
}
/**
-
换行
*/
private boolean newLine() {
// 保存上一行对象
mLineList.add(mLine);if (mLineList.size() < MAX_LINE) {// 没有超出最大行数 mLine = new Line();// 初始化新的行对象 mUsedWidth = 0;// 已使用行的宽度清零 return true; } return false;
}
/**
-
封装每一行控件
*/
class Line {private int mTotalWidth;// 当前行的总宽度(不包括水平间距) private int mMaxHeight;// 当前行的最高控件的高度 private ArrayList<View> mChildList = new ArrayList<View>(); /**
-
添加一个子元素
*/
public void addView(View childView) {
mChildList.add(childView);
mTotalWidth += childView.getMeasuredWidth();int measuredHeight = childView.getMeasuredHeight(); // 更新当前行最高控件的值 mMaxHeight = mMaxHeight < measuredHeight ? measuredHeight : mMaxHeight; } public int getChildCount() { return mChildList.size(); } // 设置子控件位置 public void layoutView(int left, int top) { int count = getChildCount(); int validWidth = getMeasuredWidth() - getPaddingLeft()
getPaddingRight();
int surplusSpace = validWidth - mTotalWidth - mHorizontalSpacing
(count - 1);// 计算剩余宽度
if (surplusSpace >= 0) {
// 有剩余空间,把剩余空间平均分配给每个子控件
int splitSpace = (int) (surplusSpace / count + 0.5f);
for (int i = 0; i < count; i++) {
View childView = mChildList.get(i);
int measuredWidth = childView.getMeasuredWidth();
measuredWidth += splitSpace;// 将多出来的宽度分配给子控件
int measuredHeight = childView.getMeasuredHeight();
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
measuredWidth, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(
measuredHeight, MeasureSpec.EXACTLY);
childView.measure(widthMeasureSpec, heightMeasureSpec);// 重写测量子控件宽高
// 如果某个控件比较矮, 要居中显示
int topOffset = (int) ((mMaxHeight - measuredHeight) / 2 + 0.5f);// 距顶端偏移量
if (topOffset < 0) {
topOffset = 0;
}
// 设置子控件的具体位置
childView.layout(left, top + topOffset, left
measuredWidth, top + topOffset + measuredHeight);
// 更新left
left = left + measuredWidth + mHorizontalSpacing;
}
} else {
// 当控件比较宽, 独自占一行时, 会走到此方法
View childView = mChildList.get(0);// 集合中只有一个控件
childView.layout(left, top,
left + childView.getMeasuredWidth(),
top + childView.getMeasuredHeight());
}
}
}
}
---------------------------------------------------------------
Day05 08.侧边栏效果DrawerLayout ##
侧边栏的实现 ##
接下来讲ActionBar,ActionBar我们一直在提,但是从来没有用过,而且它老在我们面前晃悠
现在这个谷歌电子市场的标题,其实就是actionbar,而我们当时为了上下版本兼容,是不是让我们谷歌电子市场,直接引入了一个android-support-v7-appcompat这样一个第三方的包,而这第三方包呢,也是为了让我们2.x的版本,也能够兼容actionbar的效果,
老兼容但是我们从来没用过,那我们接下来去用一下,其实我们这个项目,怎么去用到这个actionBar呢,我们先看下完整的效果,完整的效果是这样的,就在这个Actionbar的旁边,它呢,有这样三个短的横杠,你一点这三个短杆,马上弹出来一个侧脸栏,有些同学说这不就是智慧北京的SlidingMenu么,当然你去用SlidingMenu去实现侧边栏也没问题,你当然可以去实现,但是这个侧边栏是谷歌原生自带的一个侧边栏的效果,我们也了解下么,它叫DrawerLayout,
而它是配合着Actionbar,也就是标题栏使用,标题栏一点一点他就出来隐藏,出来隐藏,它是属于我们v4包下的一个控件,
那看下这个v4,在android-support-v7-appcompat下边就有一个v4,那我们看一下这个v4包的代码,在widget下有个DrawerLayout.class,这就是谷歌在v4里面专门去提供的一个侧边栏的效果,而他如何去用的呢,用法还相对不是特别复杂
怎么去用,你如果想让你的一个界面,整体能够有一个抽屉效果的话,我们就得先找到你的一个根布局,那这时候呢就找一下,我们的根布局其实就是一个简单的activity_main.xml
这时候你希望在布局的上边加一个抽屉,这时候呢你只需要把你的根布局做一个调整,而这时候你怎么调整呢,我们就让谁去做这个根布局呢,就让刚才的DrawerLayout做这个根布局
我就把这个DrawerLayout全路径拷贝过来,复制到根布局,这就是我们的抽屉的效果
那这个抽屉的布局里面,现在只有我们的主页面,那其实我们还应该有一个侧边栏,侧边栏怎么去做呢,侧边栏我们用一个帧布局来表示,怎么去表示呢,我们专门去写一个FrameLayout,而这个FrameLayout到底应该怎么去处理呢,宽高我们去处理下,宽度我们写成200dp,即我们侧边栏写死成200dp,高度填充屏幕,matchParent,而且侧边栏是在我们屏幕的左侧是吧,所以这时候可以给这个帧布局写一个Gravety属性 这个时候eclipse一点提示都没了,重启eclipse 重启之后eclipse还是没有提示,那我们就自己写
这个就是我们这样一个帧布局,我们再往帧布局中再塞一个TextView,
它这个侧边栏就直接写在根布局下边了,我们之前写SlidingMenu的时候,侧边栏是单独写一个布局文件把它塞进去的,这个就直接写在activity_main.xml中其他布局的下边
那我们再回到我们的主页面中来,即在mainactivty中初始化一下我们的侧边栏的效果,要结合我们actionbar来初始化一下,所以我们写一个方法,叫做initActionbar,来初始化actionbar效果
那我们要先拿到我们actionbar对象,即:getSupportActionBar(),当然也有api用getActionBar也能拿到,但它是4.0以上的api,我们现在要调v4包下的Actionbar
点击getSupportActionBar进去看下,这是actionbaractivity的方法,这样的话他是上下兼容的actionbar
actionbar有一个方法叫做setHomeButtonEnabled(true);等于true,表示显示logo并且可被点击,即在标题栏左上角的谷歌电子市场图片,比如我们在onCreate方法中调用一下我们的actionbar,
运行程序奔溃,看日志知道原因是在activity_main.xml布局文件中的DrawerLayout没有加宽度
加上宽高之后,“我是侧边栏”文字就出来了,logo也出来了
actionbar还有一个setDisplayHomeAsUpEnabled(true) 等于true就表示具有返回键
即在右上角有个返回的按键,一点击就返回上一个页面,其实现在有一些应用它就是通过acitonbar来做的一个返回的,而这个返回不需要你写任何的布局文件,你只需要set一个DisplayHomeAsUpEnabled等于true,它就有返回键,
同时我们这时候就要去设置我们的侧边栏,这个侧边栏叫做
ActionBarDrawerToggle(activity, drawerLayout, drawerImageRes, openDrawerContentDescRes, closeDrawerContentDescRes);
这个地方需要传activity,直接写this,还要传一个drawerLayout,这个drawerLayout我们可以findViewById拿到这个drawerLayout,即:
DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawerlayout);
然后还要传一个drawerImageRes,这个意思是就是有时候他在打开这个侧边栏的时候,左上角是三个小横杠,把它设置成R.drawable.ic_drawer_am,
然后还要传一个openDrawerContentDescRes,就指的是打开这个抽屉的这样一个文字描述,
还有closeDrawerContentDescRes,关闭抽屉的这样一个描述,
其实这个都无所谓,因为他要传一个int值,所以还必须要搞一个String进去,所以我们把Stings文件中的两个String拷贝到我们自己的strings.xml文件中。
---------------------------------------------------------
即:strings.xml
<string name="drawer_open">Open navigation drawer</string>
<string name="drawer_close">Close navigation drawer</string>
---------------------------------------------------------
这时候mToggle有个方法叫做syncState(),它的作用是将抽屉侧边栏效果和actionbar统一起来
就说因为它两要协同操作,即点actionbar就能让侧边栏出来,
这个时候我们要响应一下点击事件,一点击就让侧边栏显示或是隐藏,
activity有个方法叫onOptionsItemSelected,这表示actionbar的某个item被点击
我们现在点击的是左上角,所以应该写成android.R.id.home;
mToggle有个方法叫做onOptionsItemSelected(item),它的意思就是打开就关闭,关闭就打开
此时运行程序奔溃,看日志,No drawer view found with gravity LEFT
即这时候我们的布局可能写的有点问题,我们看一下这个布局,应该写成
<FrameLayout
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_gravity="left"
android:background="#000"
-------------------------------------------------------
在运行程序,文字颜色应该改成白色,否则看不出来,即整个布局如下:
activity_main.xml
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawerlayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<com.bq.googleplay.ui.widget.PagerTab
android:id="@+id/pager_tab"
android:layout_width="match_parent"
android:layout_height="42dp"
android:background="@drawable/bg_tab" />
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
<FrameLayout
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_gravity="left"
android:background="#000"
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是侧边栏"
android:textcolor= "#fff"
/>
</FrameLayout>
</android.support.v4.widget.DrawerLayout>
----------------------------------------------------------
我们上边在initActionbar中写了一个actionBar.setDisplayHomeAsUpEnabled(true);
表示具有返回键,那屏幕左上角这个地方它突然变成侧边栏的按钮,如果和侧边栏绑定,就起到打开关闭侧边栏的效果,而图标也是我们在这里定义的图标栏决定的
MainActivity.java中写如下
//初始化actionbar效果
private void initActionbar(){
//获取上下兼容的actionBar对象
ActionBar actionBar = getSupportActionBar();
actionBar.setHomeButtonEnabled(true);//显示logo
actionBar.setDisplayHomeAsUpEnabled(true);//具有返回键
//初始化侧边栏
DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawerlayout);
//初始化侧边栏的开关
mToggle = new ActionBarDrawerToggle(this, drawerLayout,
R.drawable.ic_drawer_am, R.string.drawer_open,
R.string.drawer_close);
mToggle.syncState();//将侧边栏效果和actionbar同步起来
}
//actionbar的某个item被点击
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home://系统预留的id,专门响应左上角按钮的点击
//左上角按钮被点击
//切换侧边栏
mToggle.onOptionsItemSelected(item);
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
最后:
并在onCreate方法中调用initActionbar();
小结:
谷歌电子市场目前上边这个标题栏,我们是使用的android原生的actionbar这样一个标题栏,那我们在做手机卫士和智慧北京的时候,并没有用到actionbar,而我们都是手动的把actionbar给禁用掉,我们通常都会使用属性notitlebar啊之类的一些属性,让它不要显示android原生的标题栏,然后是自己手动的画了一个标题栏,
在这个项目我们没有手动去画,而是完完全全按照谷歌的actionbar的标准,使用了它的actionbar
我们这个界面是将android原生标题栏actionbar和侧边栏drawerLayout一起使用的,我们可不可以单独使用标题栏actionbar?当然可以
那我如何独立去使用标题栏actionbar呢
比如说现在这个有缘网我点进去之后,上边标题栏actionbar它啥都不做,就是actionbar,那我现在就希望在这个actionbar上加一个返回的效果,让它一点返回键,这个有缘网详情页面就退出么
来到HomeDetailActivity.java
在这个地方也写一个初始化actionbar的这样一个方法initActionbar(),
那我们先拿到actionbar对象,即:getSupportActionBar();
这时候要让actionbar有返回的效果,那这时候给它setDisplayHomeAsUpEnabled(true);即可
然后在onCreate方法中调用此方法initActionbar()初始化好即可。
而这个返回键点击之后要做什么处理呢,那这时候想用这个actionbar的按钮的点击事件
怎么去响应呢,这样去响应,再写一个onOptionsItemSelected(MenuItem item)方法,
即当我们的选项,这都是它回调一个MenuItem,其实我们点的这个左上角的地方,它就是一个item,然后我们从这个进行一个switch判断,看他是什么id,即item.getItemId()
那这时候我们的id是一个预留的id,即R.id.home,这就是我们左上角被点击,而当左上角被点击的时候,我们直接将当前页面给finish掉即可。
这就是我们纯粹使用actionbar的效果,那我们可以给actionbar效果再做一点点小小的扩充,
比如说有些同学会发现,有时候标题栏的右侧,也会有些小按钮,一点击之后,可能有相应的一个效果,那个小按钮又是怎么添加的呢,你现在光给我在左上角添加了一个返回键,而右上角的按钮一点我想让他弹一个popwindow,或者是跳一个activity, 怎么去做呢,这时候我们就看这个东西,比如说我们在创建一个项目的时候,回顾一下我们平时创建项目是什么创建的,我们创建一个项目之后,我们通常会做什么事情呢,我们通常做的事情是直接将onCreateOptionsMenu(Menu menu)方法给删了,这个方法到底是干什么用的呢,为什么我一创建它就给我搞一个这样的方法,他应该有他的道理,这个方法其实就是actionbar的一个相关的方法,
它里边做了一个什么事情呢,它里边是一个Menu,我们讲基础的时候,可能提到过Menu菜单的一个创建吧,当时好像就是在这个方法中去搞的吧 但是Menu菜单现在已经过时了,就说我们现在很少发现,有应用它点击menu菜单,把核心功能它都放在了menu菜单中,为什么它过时了呢,是因为它隐藏的太深了,很多用户在玩手机的时候,它只是页面上玩,页面上有什么按钮 最多它只知道一个返回键,而Menu菜单它很不容易点击进去,所以它隐藏的比较深,所以慢慢被时代所淘汰了
所以你现在发现的很多最新款的手机,都没有Menu菜单这样的物理键,这是现在时代的一个发展趋势,当然有同学说老师你不对,人家qq啊什么的就是用menu菜单,人家一点就能出来东西,
这里我所说的东西是Menu菜单里面即使会出来东西,也是那些往往在menu菜单中有的东西,往往在界面上也是有另外一个入口,就说你在玩软件的时候,不一定你这个退出只能在这个地方退出,当然有时候可能会有好几个入口,都能达到你同样的一个操作,所以说你menu菜单中的一些东西,
我们在界面往往能找到别的入口,之所以menu菜单中还有东西,是为了满足习惯于使用Menu菜单的用户,这样用户使用起来很方便,
还有一个原因是什么呢,有时候有些功能是产品经理不希望用户看到,但是它又不能没有,哪些功能产品经理不希望用户点呢,比如说退出,注销,所以你会发现qq的退出按钮是在menu菜单中,让人不容易找到
那现在Menu菜单被淘汰了,哪个东西火起来了呢,谷歌就建议使用actionbar,actionbar因为它都是显示在标题栏上边的,所以它把menu中的东西全都列在这个地方,这就是actionbar
而我们现在再分析下这个方法,即onCreateOptionsMenu(Menu menu),做actionbar的时候也是用这个方法,它getMenuInflater().inflate了一个什么布局呢,(R.menu.main,menu)
你会发现创建一个项目之后,它里边有个menu这样的文件夹吧,你都不知道那是干嘛的,这时候你点这个menu进去到main.xml中,它跟标签是不是这个menu,而这个item就是我们actionbar上面要显示的item
我们这时候在来到这个main.xml布局中来,仔细分析下它的这几个属性,一个叫做android:orderInCategory="100",这个是排序,比如说我们有时候标题栏右上角上有好几个按钮,谁在前边谁在后边,我们有个优先级,那你这个数字越大,它排在前边的可能性就越大
android:title="@string/action_settings",这个title就是我们的字符串,你要显示什么文字就用它
android:showAsAction="never",它就是说你展现的方式,在这个地方的话,它写的是never
表示从不展现,你既然写一个actionbar,你让他从不展现,那你让他干嘛啊,它这个地方的从不展现指的是什么,它是指的以隐藏的方式展现,而不是完完整整的放在这个标题栏右上角的地方,
什么是隐藏,就是你点击标题栏右上角的按钮,它才把这个Settigns显示出来,它不是说直接在标题栏右上角搞这么一个Settings按钮
那还有别的展现方式,比如always,它是让一直展现,即在标题栏右上角一上来就展现,有些同学说,老师我是图标啊,不是文字,人家是可以定义图标的,即属性:android:icon="@drawable/ic_launcher",那这时候你去运行一下,标题栏右上角就变成图片了 而我们settings这个文字在这个地方就没有什么意义了吧,而我如果要把文字显示出来,怎么显示呢,你去长按这个图标,它就会弹出一个Settings的文字,就和你在网页上去玩的时候,你会发现你把鼠标浮在那个地方,那个地方的注释是不是就出来了,android也有这样的一个特点,你长按注释就出来了,这是一个actionbar的效果,那有些同学说我怎么去响应这个右上角item的点击事件呢,怎么去响应,我们响应点击事件,还是用的那个onOptionsItemSelected(MenuItem item)方法,这个方法还是写个Switch,还是item.getItemId(),我们在谷歌电子市场中标题栏左上角返回键用的这个id是系统默认的id,但是现在标题栏右上角是我自己写的一个item,它的id我们在布局文件main.xml中定义了即action_settings,所以在这个地方我判断是R.id.action_settings,
最后再讲一下,这个actionbar到底常用还是不常用?
actionbar不常用,因为actionbar是谷歌官方提出来的一个东西,它是希望开发者用的,
从哪里能体现出它希望你用呢,就从你创建项目的时候,你还没说呢,他就非常热情的把onCreateOptionsMenu方法给你搞过来了,已经在menu文件中把这个main.xml布局文件给你创建好了,仅从这一点就可以看出,它特别热切的希望你用actionbar功能,
但是我们在开发智慧北京,在前边开发任何项目的时候,我们连说都不说,直接就把这个方法删掉了,那当时为什么要删掉呢,我们在实际开发中到底要不要用呢,不用,外边有10家公司,可能有9家公司都不用actionbar,为什么不用actionbar,因为发现 我之前也用过actionbar,包括开发那个搜狗问问客户端,那个标题全都是用actionbar搞的,我当时搞的都快吐血了,为什么这个actionbar不好搞呢,是因为actionbar,你看它的特点很明显,它的logo都是展示在标题栏左上角,它的标题文字也都是展现在标题栏左上角,而标题栏右侧的话,是不是展现的是一个一个的功能图标啊 这是actionbar的一个标志,那这时候产品经理过来说,咱们能不能够把这个logo就不要在这个地方展示了,我想把logo居中,对不起,不好意思,不好搞,也不是说办不到,办起来真费劲,项目经理过来说这个标题栏背景是黑色好丑啊,那换成蓝色或者红色,能不能搞呢,肯定能搞,但是很麻烦,你看我平时如果自己写一个布局的话,分分钟给它设置一个背景,马上出来了吧
但是你要改actionbar的背景的话,没那么简单啊,你只能给它设置样式啊,有时候你给一个布局设置style样式吧,你就在style.xml中写这个样式,把这个背景的样式啊,什么的样式什么的样式全都给它写出来,这个写法还比较复杂,具体的规则 你要说背景还必须搞背景的那个样式属性给它设置
或者有些同学说,老师啊,这个返回按钮人家美工说太丑了,要换,不好意思,也得换样式,
或者还有同学说,老师啊,标题栏右边有个图标,然后它旁边也有个图标,可我觉得他两离的有点太远,有点太近,尤其是遇到那种死扣的美工,多了一个像素少了一个像素,能不能调一下,对不起,很难调,或者你觉得这个图标打了或者小了,能不能缩小点,放大点,很难搞,不是说搞不了,但是搞起来很麻烦,比我们的传统布局搞起来费劲多了,所以的话,这个actionbar,我们国内很多的厂商它是不喜欢用actionbar,因为它定制起来太麻烦,我如果自己写一个布局,也可以
画出和actionbar一样的这样一个东西,我就一个iamgeView,imageView,textview么,
干嘛还得用你的actionbar,去了解你的actionbar的api啊,所以你看我们智慧北京,手机卫士这些标题栏根本就用不着actionbar,都是我们自己写的,而且很容易改,所以的话,不是很常用,那为什么还有一小部分公司用呢,有些公司逼格很高,觉得我们应该完全按照谷歌官方标准去走,你会发现谷歌的软件,确实全都是在用actionbar,但是你们可能没有用过,谷歌所有的软件在国内都被墙了,
怎么区分谷歌的actionbar和自己画的标题栏?
区分方式就是你找到它标题栏右侧的一个按钮,然后长按,看看有没有一个toast提示,有的话八成就是actionbar,为什么呢,你想象一下,你如果自己画一个布局,你还给这个item搞一个长按事件让它弹出一个toast,你有病啊是吧,一般人它不会这样去搞得对吧,只有谷歌它自带默认的本来就有这个toast,而且一把用户,我不说的话,谁知道这个还可以长按,基本不知道吧,你知道微信那个标题栏上有个放大镜图片的按钮吧,你长按它它是不是就出来一个“搜索”的toast,所以证明微信它就是用actionbar来做的,有些同学说老师我长按旁边的加号,不弹出toast提示,这个有时候他是对默认item会弹出来,但有时候会对一些item,做一些小小的定制,微信加号旁边有时候会显示一些其他的东西,比如显示一个小红点啊什么的,但是只要发现有一个能够弹出来,那他基本就能确定它是在使用actionbar效果,不信回去你把微信的代码反编译一下,你会发现微信它也用的是android-support-v7-appcompat这个包,因为它也要做上下兼容,他也引入的是这个包,引入它的话,八成都是在用actionbar,这就是我们区分的一个技巧。微信逼格比较高,用的是actionbar,别的软件基本没怎么去用,这个就是我们对actionbar的这样一个补充。