前言
Fragment的应用越来越多,除了一些常用的:标签切换,引导页,广告位等,目前我们项目中,现在一些大大小小的自定义的控件,也先封装在Fragment中,然后在通过Activity来显示和隐藏,这样也切切实实的到达Fragment复用的效果,Activity的代码也少了。比如一些dialog、列表框、自定义的音量条等。这篇主要是对Fragment的应用,如果对Fragment的基础还不是很熟悉的话,可以移步到这里,有详细的介绍。
开始
这篇也是对上一篇基础的加强,本篇介绍一个屏幕设配的栗子来解析,代码在最后也会给出。主要是在开发TV时,经常会有手机和电视端共用apk,那么就需要对屏幕的大小需要设配了。
该实例同时也是借鉴官方的例子,实现了屏幕横竖的切换适配,在最后再添加了Android pad大屏幕的支持。
首先,一般第一步就是新建一个MainActivity,用于控制布局的显示。其布局文件activity_main.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/titles"
android:name="com.gotechcn.fragmentdemo.TitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
这里引入了一个TitleFragment,是用于显示标题布局,这个后面会介绍。这个布局默认用于显示竖屏时的布局文件,那么横屏的布局文件是什么样的?
在res目录下新建layout-land目录,然后这个目录下创建新的activity_main.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="horizontal">
<fragment
android:id="@+id/titles"
android:name="com.gotechcn.fragmentdemo.TitleFragment"
android:layout_width="0px"
android:layout_height="match_parent"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/details"
android:layout_width="0px"
android:layout_height="match_parent"
android:layout_weight="2"
android:background="?android:attr/detailsElementBackground"/>
</LinearLayout>
这个布局和前面的布局,多了一个FrameLayout,这个布局是用来显示Detail的内容。当屏幕纵向显示时,系统会应用该布局。
现在,开始先来实现第一个Fragment:TitleFragment,具体的代码如下:
/**
* 用于显示Title的布局
*
* 直接继承ListFragment
* 可以少些一些代码,也可以继承Fragment,但就需要自己写ListView + 适配器了;
* 继承了ListView,则可以不重写onCreateView()方法;
*/
public class TitleFragment extends ListFragment
{
/**
* 初始化化数据
*/
private String [] mTitles = {"IPTV", "VOD", "YOUTUBE", "DVB", "KTV"};
private Callbacks mCallbacks = null;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mCallbacks = (Callbacks) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement Callbacks");
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
//添加设配器,默认的布局,也可以自定义布局显示
setListAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_activated_1, mTitles));
//设置为单选模式
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id)
{
super.onListItemClick(l, v, position, id);
mCallbacks.onItemClick(position);
}
/**
* item点击回调,在MainActivity实现该接口
*/
public interface Callbacks
{
void onItemClick(int index);
}
public void setCallbacks(Callbacks callbacks){
mCallbacks = callbacks;
}
}
代码很简单,简单说说,顺便补充一下知识点,主要有2个知识点:
1.如何实现ListFragment;
2.如何和Activity实现通信交互;
首先:
一般,TitleFragment是继承Fragment,但这里既然需要一个List的列表,那么就可以直接继承ListFragment,它已经帮我们准备好一切,这样子我们就可以不用自己去添加ListView去实现了,所以也不用实现onCreateView()方法。
实现ListFragment也比较简单,和listView的差不多:
首先准备数据mTitles ;
通过setListAdapter()设置简单的适配器;
最后实现onListItemClick()方法。
搞定!!!
PS:Fragment一些其他直接的子类,也是比较常用的:
DialogFragment
显示浮动对话框。使用此类创建对话框可有效地替代使用 Activity 类中的对话框帮助程序方法,因为您可以将Fragment对话框纳入由 Activity 管理的Fragment返回栈,从而使用户能够返回清除的Fragment。
ListFragment
显示由适配器(如 SimpleCursorAdapter)管理的一系列项目,类似于 ListActivity。它提供了几种管理列表视图的方法,如用于处理点击事件的 onListItemClick() 回调。
PreferenceFragment
以列表形式显示 Preference 对象的层次结构,类似于 PreferenceActivity。这在为您的应用创建“设置” Activity 时很有用处。
其次:
这里添加一个回调的方法,这是Fragment之间通信的一种方式。其实,对于跳转到DetailFragment的逻辑处理,也可以直接在TitleFragment处理(官方的例子是这样子的),通过getActivity()的方式获取宿主Activity的实例。但Fragment与Fragment的通信是需要借助Activity,所以我个人,还是比较喜欢直接用回调的方法,将逻辑处理放在Activity。
为确保宿主 Activity 实现此接口,在 onAttach() 回调方法(系统在向 Activity 添加Fragment时调用的方法)会通过转换传递到 onAttach() 中的 Activity 来实例化 Callbacks 的实例。如果 Activity 未实现接口,则片段会引发 ClassCastException。
对于接口的实现也可以通过在Activity中实现setCallbacks(Callbacks callbacks)方法,将当前的Activity传给参数。
该回调接口将会在MainActivity中实现,当用户点击TitleFragment列表项时,系统都会调用TitleFragment中的 onListItemClick(),然后该方法会调用 onItemClick() 与 Activity 共享事件。
到此,TitleFragment的解析就差不多了。
接下看看DetailFragment的实现,代码如下:
public class DetailsFragment extends Fragment
{
String[] mDetails = { "IPTV", "VOD", "YOUTUBE", "DVB", "KTV" };
/**
* 相当于构造器
* 传入需要的参数,设置给arguments
*/
public static DetailsFragment newInstance(int index)
{
DetailsFragment f = new DetailsFragment();
Bundle args = new Bundle();
args.putInt("index", index);
f.setArguments(args);
return f;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_detail, container, false);
TextView text = (TextView) view.findViewById(R.id.textView);
text.setText(mDetails[getShownIndex()]);
return view;
}
/**
* 获取当前item的位置
*/
public int getShownIndex()
{
return getArguments().getInt("index", 0);
}
}
在onCreateView()方法中,获取数据动态的设置布局,这里的布局比较简单,只有一个TextView,对应的fragment_detail.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">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:textSize="40sp"
android:textColor="@android:color/holo_blue_light"
android:text="TextView"/>
</RelativeLayout>
public class MainActivity extends Activity implements TitleFragment.Callbacks
{
/**
* 是否支持双屏模式,即标题和详情在两边同时显示出来,默认是不支持
*/
private boolean mTwoPane = false;
/**
* 当前标题的索引值
*/
private int mCurCheckPosition = 0;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 屏幕的旋转,可以保存状态,通过状态的保存,获取数据
if (savedInstanceState != null)
{
mCurCheckPosition = savedInstanceState.getInt("index", 0);
}
//判断是否双屏界面:获取“详情”的布局控件是否显示
View detailsFrame = findViewById(R.id.details);
mTwoPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;
//也可以通过该方式,让Activity实现回调
// ((TitleFragment)getFragmentManager().findFragmentById(R.id.titles)).setCallbacks(this);
if (mTwoPane){
showDetails(mCurCheckPosition);
}
}
/**
* 实现Callbacks的回调方法,显示Detail页面的内容
* @param index
*/
@Override
public void onItemClick(int index) {
showDetails(index);
}
/**
* 显示“详情”布局
* @param index 对应标题的索引
*/
void showDetails(int index)
{
mCurCheckPosition = index;
//如果是双屏模式
if (mTwoPane){
/**
* 通过FragmentManager()获取DetailsFragment布局
*/
DetailsFragment details = (DetailsFragment) getFragmentManager().findFragmentById(R.id.details);
if (details == null || details.getShownIndex() != index)
{
/**
* 传入不同的index,来创建不同的Fragment
* 这样子可以减少Fragment的创建;
* 不过,前提Fragment的布局功能大体相同
*/
details = DetailsFragment.newInstance(index);
//通过事务提交
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.details, details);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
}
}else //不是双屏模式,则跳到另一个DetailsActivity,实现单屏模式
{
Intent intent = new Intent();
intent.setClass(MainActivity.this, DetailsActivity.class);
intent.putExtra("index", index);
startActivity(intent);
}
}
/**
* 保存位置状态,不然每次detail的内容多是第一个
* @param outState
*/
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putInt("index", mCurCheckPosition);
}
}
代码上大部分以添加了注释,应该明白大体的流程,这里简单梳理一下:
首先系统会自动检测当前屏幕是横屏还是竖屏,然后会显示相应的main_activity.xml布局文件,那么就开始判断是否添加了details这个元素;
如果添加了,那么当前就是横屏,属于双屏模式,那么就在details元素的布局中添加DetailsFragment布局;
如果没有添加,那么当前就是竖屏,属于单屏模式,那么就直接跳到DetailActivity。
其中,detail需要判null,主要是因为,当Activity因为配置发生改变(屏幕旋转)或者内存不足被系统杀死,造成重新创建时,我们的fragment会被保存下来,但是会创建新的FragmentManager,新的FragmentManager会首先会去获取保存下来的fragment队列,重建fragment队列,从而恢复之前的状态。
那接下来看看DetailActivity的实现:
/**
* 单屏模式下
*/
public class DetailsActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_details);
/**
* 屏幕旋转会重新创建该Activity
* 如果屏幕从单屏旋转到双屏模式下,则销毁当前Activity,进入双屏模式
*/
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
finish();
return;
}
/**
* 向当前的Activity的动态实现DetailsFragment
*/
if (savedInstanceState == null)
{
DetailsFragment details = DetailsFragment.newInstance(getIntent().getIntExtra("index", 0));
getFragmentManager().beginTransaction().add(R.id.layout_details, details).commit();
}
}
}
如果是单屏模式,那么就在R.layout.activity_details添加DetailsFragment布局。
这里添加一个横竖屏的判断,当屏幕旋转时,会重新执行onCreate()方法,如果需要旋转到横屏,那么就finish掉当前的Activity。
好了,代码的详细的分析就就到此结束了,现在我们来看看最后的实现效果:
竖屏时:
旋转屏幕时:
代码写到这了,我们还可以直接适配Android pad(不考虑pad的横竖屏,只考虑屏幕的大小),其实和前面的横竖屏的适配几乎一样,只要添加一个布局目录就可以直接实现了。
在res目录新建一个layout-large目录,然后将layout-land目录下的布局直接复制在该目录下,就可以了,效果图如下,横竖多是一样的布局:
好了,栗子已经啃完了,其实比较简单,实际开发中,功能也会比较复杂,对于横竖屏切换的细节还要多考虑,比如一些布局需要判断是否为null等问题。
如有异议,欢迎指出,谢谢。