文章目录
在进行安卓开发的时候不仅需要针对手机,还要针对平板进行适配,而平板的屏幕尺寸普遍比手机要大得多,因此软件UI在手机上适配的话有可能在平板上不适配,会带来视觉上较大的差异。
自动Android 3引入碎片的概念,能更好地在平板上显示UI。
出自《第一行代码(第二版)》,用于自己学(chao)习(shu)记录
碎片
碎片是一种可以嵌入到活动中地UI片段,可以让程序更加合理地利用大屏幕的空间。碎片和活动有些类似,都有着自己的布局和生命周期,可以看作是迷你型的活动。
举个栗子,一个新闻软件,使用RecyclerView控件显示新闻标题列表,点击标题后会跳转到新闻详情,使用了两个活动,如图所示
而如果直接放到平板上,标题列表会横向拉伸,很不美观,较好的设计是将标题列表和详情分别放到两个碎片中,这样就看起来会好很多
使用
新建两个碎片布局
分别编写left_fragment.xml和right_fragment.xml两个布局
filename:left_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/Button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="按钮"
/>
</LinearLayout>
filename:right_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#AAAA00">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/TextView1"
android:layout_gravity="center_horizontal"
android:text="这是一个TextView"/>
</LinearLayout>
右边碎片是个屎黄色背景。
然后编写碎片的类,LeftFragment类和RightFragment类,需要继承Fragment类。继承android.support.v4.app.Fragment而不是android.app.Fragment,因为如果使用系统内置的有些低版本会无法使用一些功能,而且v4库在gradle中已经引入过了。
这里只是重写了onCreateView()方法,在其中动态加载布局即可。
filename:LeftFragment.java
public class LeftFragment extends Fragment{
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.left_fragment,container,false);
return view;
}
}
filename:RightFragment
public class RightFragment extends Fragment{
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.right_fragment,container,false);
return view;
}
}
然后在主活动布局文件中添加碎片控件,通过android:name来指定类名
filename:first_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_width="0dp"
android:id="@+id/fragment1"
android:layout_height="match_parent"
android:layout_weight="1"
android:name="com.example.k.androidpractice_1.LeftFragment"
/>
<fragment
android:id="@+id/fragment2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:name="com.example.k.androidpractice_1.RightFragment"
/>
</LinearLayout>
这时候新建一个平板虚拟机
运行程序,效果如图
动态添加碎片
碎片的强大之处在于可以在程序运行的时候动态添加到活动中。
新建andther_right_fragment.xml
filename:another_right_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/TextView2"
android:layout_gravity="center_horizontal"
android:text="这是另一个碎片"/>
</LinearLayout>
然后再新建一个AnotherRightFragment.java,继承v4库的Fragment,同样重写onCreateView()方法,动态加载布局
public class AnotherRightFragment extends Fragment{
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.another_right_fragment,container,false);
return view;
}
}
修改first_layout.xml,更换右边的碎片控件为FrameLayout,这个布局所有控件默认放置左上角,是最简单的布局,在这里用比较适合。
filename:first_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_width="0dp"
android:id="@+id/fragment1"
android:layout_height="match_parent"
android:layout_weight="1"
android:name="com.example.k.androidpractice_1.LeftFragment"
/>
<FrameLayout
android:id="@+id/FrameLayout1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
/>
</LinearLayout>
随后修改FristActivity.java,给Button添加了一个监听,在onCreate()中添加RightFragment类,监听Button,点击按钮切换到AnotherRightFragment碎片类。
在replaceFragment中,切换碎片需要五个步骤
- 创建待添加的碎片实例
- 获取FragmentManagement实例,通过getSupportFragmentManager()方法获取
- 开启事务,调用beginTransaction()方法实现
- 向容器内添加或替换碎片,通过replace()方法实现,第一个参数为容器id,第二个参数为待添加或替换碎片实例
- 调用commit()方法实现事务的提交
这样就可以实现碎片的切换了。
filename:FirstActivity.java
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout);
Button MyButton=findViewById(R.id.Button1);
MyButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceFragment(new AnotherRightFragment());
}
});
replaceFragment(new RightFragment());
}
private void replaceFragment(Fragment fragment){
FragmentManager fragmentManager=getSupportFragmentManager();
FragmentTransaction fragmentTransaction=fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.FrameLayout1,fragment);
fragmentTransaction.commit();
}
这时候再次运行程序,点击Button可以发现右边的碎片成功切换了。
碎片返回栈
但是此时按返回键的时候会发现程序退出了,此时需要模拟活动的返回栈,即按返回后回到上一个碎片。
可以通过FgmentTranscration中的addToBackStack()方法,将一个事务添加到返回栈中。
修改FirstActivity.java代码,添加fragmentTransaction.addToBackStack(null);
即可
。。。
private void replaceFragment(Fragment fragment){
FragmentManager fragmentManager=getSupportFragmentManager();
FragmentTransaction fragmentTransaction=fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.FrameLayout1,fragment);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
}
。。。
现在再运行程序以后,点击Button切换了碎片,按返回键后先返回到上一个碎片,然后再按一次才退出。
碎片的通信
碎片和活动是处于独立的类中,相互不直接影响,没有直接通信的方式,如果想在活动中调用碎片或者在碎片中调用活动,FragmentManager提供了方法。
类似findViewById()的findFragmentById()方法用于从布局文件中获取碎片实例,如RightFragmet rightFragment=(RightFragment)getSupportFragmentManager().findFragmentById(R.id.right_Fragment);
在碎片中调用与之关联的活动:MainActivity activity=(Activity)getActivity();
。如果碎片中需要使用到context对象的时候,也可以使用这个方法,因为Activity本身就是一个context。
所以碎片之间的通信就可以实现了,在碎片中调用与之关联的活动,在活动中获取另一个碎片的实例,进行通信。
碎片生命周期
活动的生命周期中有四个状态:运行状态、暂停状态、停止状态和销毁状态。同样碎片也有这四个运行状态
- 运行状态:碎片可见,且关联活动和碎片均处于运行状态
- 暂停状态:活动进入暂停状态时(如另一个未占满整个屏幕的活动处于栈顶),与之关联的可见碎片进入暂停状态
- 停止状态:活动进入停止状态的时候,与之关联的碎片也进入停止状态。或者调用FragmentTransaction的remove()、replace()方法将碎片从活动中移除。若在事务提交之前调用addToBackStack()方法,碎片也会进入停止状态。停止状态时完全不可见的,还有可能被系统回收。
- 销毁状态:活动被销毁的时候与之关联的碎片也被销毁了。如果在事务提交之前没有调用addToBackStack()方法,碎片也会进入销毁状态。
活动中有的回调方法碎片中几乎都有,碎片中还有一些额外的方法:
- onAttach():碎片与活动关联的时候调用
- onCreateView():为碎片创建视图(加载布局)的时候调用
- onActivityCreated():与碎片关联的活动创建完毕的时候调用
- onDestroyView():与碎片关联的视图被移除的时候调用
- onDetch():碎片与活动解除关联的时候调用
如下图
动态加载布局的技巧
动态加载碎片的功能很强大,其实只是在布局文件中的添加和替换操作,如果可以根据屏幕分辨率或大小决定加载什么布局用户体验会更好。
限定符
在使用平板的时候很多应用都是横屏双页的,手机则是单页,如何判断应该是双页还是单页呢,需要使用到限定符了。
修改layout文件夹下的first_layout.xml,只留下左边的一个碎片
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_width="match_parent"
android:id="@+id/LeftFragment"
android:layout_height="match_parent"
android:name="com.example.k.androidpractice_1.LeftFragment"
/>
</LinearLayout>
新建文件夹layout-large,新建first_layout.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">
<fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/LeftFragment"
android:name="com.example.k.androidpractice_1.LeftFragment"
/>
<fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
android:name="com.example.k.androidpractice_1.RightFragment"
android:id="@+id/RightFragment"
/>
</LinearLayout>
会根据设备自动选择是单页还是双页,但是这里可能是我手机太大了?他还认为我是双叶,然后给我都显示出来了,目前还不知道是为什么,讲道理手机应该只显示按钮的
一般的限定符
- small - 小屏幕设备 - ldpi - 低分辨率设备(120dpi以下)
- normal - 中等屏幕设备 - mdpi - 中分辨率设备(120dpi~160dpi)
- large - 大屏幕设备 - hdpi - 高分辨率设备(160dpi~240dpi)
- xlarge - 超大屏幕设备 - xhdpi - 超高分辨率设备(240dpi~320dpi)
- xxhdpi - 超超高分辨率设备(320dpi~480dpi)
- land - 横屏设备
- port - 竖屏设备
最小限定符
文件夹名字为layout-sw600dp的时候,当设备屏幕宽度大于等于600dp的时候会加载这个文件夹中的布局。
实战 - 简易版新闻应用
需要使用到RecyclerView控件,先添加好依赖
compile ‘com.android.support:recyclerview-v7:24.2.1’
编写一个新闻类News.java
filename:News.java
public class News {
private String Title;
private String Content;
public String getTitle(){
return Title;
}
public String getContent(){
return Content;
}
public void setTitle(String Title){
this.Title=Title;
}
public void setContent(String Content){
this.Content=Content;
}
}
然后新建新闻碎片的布局文件news_conteng_frag.xml
filename:news_content_frag.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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="match_parent"
android:id="@+id/visiablity_layout"
android:visibility="invisible"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/NewsTitle"
android:gravity="center"
android:padding="10dp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000"/>
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/NewsContent"
android:layout_weight="1"
android:padding="15dp"/>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:background="#000"/>
</RelativeLayout>
这里主要有两部分,一个是新闻标题,另一个是新闻内容,View是用来画线的,这个RelativeLayout就是左边的标题列表,第二个View是列表右边的竖线,更美观。
新建一个碎片类NewsContentFragment,动态加载碎片布局,然后获取标题和内容的TextView,设置对应的新闻标题和正文
filename:NewsContentFragment.java
public class NewsContentFragment extends Fragment{
private View view;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
view=inflater.inflate(R.layout.news_content_frag,container,false);
return view;
}
public void refresh(String NewsTitle,String NewsContent){
View VisibilityLayout=view.findViewById(R.id.visiablity_layout);
VisibilityLayout.setVisibility(View.VISIBLE);
TextView TitleTextView=view.findViewById(R.id.NewsTitle);
TextView ContentTextView=view.findViewById(R.id.NewsContent);
TitleTextView.setText(NewsTitle);
ContentTextView.setText(NewsContent);
}
}
上面这个是双页模式用的,现在编写单页模式的布局文件,新建活动NewsContentActivity,布局文件为news_content
filename:news_content.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.k.androidpractice_1.NewsContentActivity">
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/news_content_fragment"
android:name="com.example.k.androidpractice_1.NewsContentFragment"/>
</LinearLayout>
修改NewsContentActivity.java内容,通过actionStart(…)方法可以很好的接收参数并且启动活动,在onCreate()中,先从Intent中获取到新闻的标题和正文,随后通过id获取碎片实例,刷新标题和正文。
filename:NewsContentActivity.java
public class NewsContentActivity extends AppCompatActivity {
public static void actionStart(Context context, String NewsTitle, String NewsContent){
Intent intent=new Intent(context,NewsContentActivity.class);
intent.putExtra("news_title",NewsTitle);
intent.putExtra("news_content",NewsContent);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_content);
String NewsTitle=getIntent().getStringExtra("news_title");
String NewsContent=getIntent().getStringExtra("news_content");
NewsContentFragment newsContentFragment=(NewsContentFragment)getSupportFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(NewsTitle,NewsContent);
}
}
新建一个布局文件用于显示新闻列表,news_title_frag.xml
filename:news_title_frag.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/TitlrRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
编写子项布局news_item.xml,只有一个TextView,android:ellipsize
就是说一行显示不下的时候省略
filename:news_item.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/news_title"
android:ellipsize="end"
android:padding="10dp"
/>
现在需要展示新闻列表的碎片了,新建NewsTitleFragment类,就是把RecyclerView控件动态加载进来了,然后在onActivityCreated()方法中通过对news_content_frag碎片的获取判断当前是双页模式还是单页模式。
filename:NewsTitleFragment.java
public class NewsTitleFragment extends Fragment{
private boolean IsTwo;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.news_title_frag,container,false);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_fragment)!=null)
IsTwo=true;
else
IsTwo=false;
}
}
之后修改主布局文件first_layout.xml内容
filename:first_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/news_title_frag"
android:name="com.example.k.androidpractice_1.NewsTitleFragment"/>
</LinearLayout>
然后新建一个文件夹layout-sw600dp,新建first_layout.xml,两个碎片,第一个碎片是标题列表,第二个碎片是新闻正文。
filename:layout-sw600dp/first_layout.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">
<fragment
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/news_title_frag"
android:name="com.example.k.androidpractice_1.NewsTitleFragment"/>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
>
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.example.k.androidpractice_1.NewsContentFragment"
android:id="@+id/news_content_frag"/>
</FrameLayout>
</LinearLayout>
然后就是最麻烦的了,给标题列表添加适配器,好是好麻烦!!!
filename:NewsTitleFragment.java
package com.example.k.androidpractice_1;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
/**
* Created by kang on 2020/1/30.
*/
public class NewsTitleFragment extends Fragment{
boolean IsTwo;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.news_title_frag,container,false);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_fragment)!=null)
IsTwo=true;
else
IsTwo=false;
}
class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder>{
private List<News> MyNewsList;
class ViewHolder extends RecyclerView.ViewHolder{
TextView MyTextView;
public ViewHolder(View view){
super(view);
MyTextView=view.findViewById(R.id.news_title);
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view=LayoutInflater.from(parent.getContext()).inflate(R.layout.news_items,parent,false);
final ViewHolder holder=new ViewHolder(view);
view.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
News New=MyNewsList.get(holder.getAdapterPosition());
if (IsTwo){
NewsContentFragment newsContentFragment=(NewsContentFragment)getFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(New.getTitle(),New.getContent());
}else{
NewsContentActivity.actionStart(getActivity(),New.getTitle(),New.getContent());
}
}
});
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
News New=MyNewsList.get(position);
holder.MyTextView.setText(New.getTitle());
}
@Override
public int getItemCount() {
return MyNewsList.size();
}
public NewsAdapter(List<News> MyNewList){
this.MyNewsList=MyNewList;
}
}
}
再次修改NewsTitleFragment类中的onCreateView()方法,添加getNews()方法,为了添加新闻信息,没什么大用
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.news_title_frag,container,false);
RecyclerView NewTitleRecyclerView=view.findViewById(R.id.TitlrRecyclerView);
LinearLayoutManager linearLayoutManager=new LinearLayoutManager(getActivity());
NewTitleRecyclerView.setLayoutManager(linearLayoutManager);
NewsAdapter Adapter=new NewsAdapter(getNews());
NewTitleRecyclerView.setAdapter(Adapter);
return view;
}
private List<News> getNews(){
List<News> NewsList=new ArrayList<>();
for (int i=0;i<50;i++){
News New=new News();
New.setTitle("this is title - "+i);
New.setContent("this is title - "+i+"this is title - "+i+"this is title - "+i+"this is title - "+i+"this is title - "+i+"this is title - "+i);
NewsList.add(New);
}
return NewsList;
}
之后运行就可以看到结果了,平板上看是这样子的。