更多动态视图MoreNewsView
经常看朋友圈的动态,有的动态内容较多就只展示前面一段,如果用户想看完整的再点击展开,这样整个页面的动态列表比较均衡,不会出现个别动态占用大片屏幕的情况。同样,查看博客的文章列表也类似,只展示文章开头几行内容,有需要再点击加载全篇文章。
动态列表直接使用ListView,动态内容就得自己写个控件了,自定义控件的难点在于如何把握动态下拉和收起的动画。这里我们要先预习TextView的相关函数,下面是本文用到的方法说明:
getHeight : 获取TextView的显示高度。
setHeight : 设置TextView的显示高度。
getLineHeight : 获取每行文本的高度。
getLineCount : 获取所有文本的行数。
如果一开始每条动态默认显示四行,那么默认显示高度是getLineHeight*4,使用setHeight方法即可设置动态的初始显示高度。点击展开动态全文时,就得显示所有行的文本,整个文本的高度是getLineHeight*getLineCount。现在有了每条动态的初始高度,以及动态全文的完整高度,再加个拉伸动画就差不多了。拉伸动画的主要工作是随着时间的推移,给TextView设置渐增或渐减的高度,这要重写Animation的applyTransformation方法。
下面是点击监听器的显示动画代码示例:
private OnClickListener mOnClickListener = new View.OnClickListener() { boolean isExpand; @Override public void onClick(View v) { tv_expand.setText(isExpand?"查看全文":"收起关注"); isExpand = !isExpand; tv_content.clearAnimation(); final int deltaValue; final int startValue = tv_content.getHeight(); int durationMillis = 300; if (isExpand) { deltaValue = tv_content.getLineHeight() * tv_content.getLineCount() - startValue; } else { deltaValue = tv_content.getLineHeight() * maxLine - startValue; } Animation animation = new Animation() { protected void applyTransformation(float interpolatedTime, Transformation t) { tv_content.setHeight((int) (startValue + deltaValue * interpolatedTime)); } }; animation.setDuration(durationMillis); tv_content.startAnimation(animation); } };
下面是展开/收起朋友圈动态详情的效果截图
可折叠列表ExpandableListView
嵌套列表ExpandableListView是又一种常见的控件,常见的业务场景包括:好友分组与好友列表、订单列表与订单内的商品列表、邮件夹分组与邮件列表等等。
ExpandableListView常用方法
Android自带的ExpandableListView可以直接用于嵌套列表,点击一个组,展开该组下的子列表;再点击这个组,收起该组下的子列表。
下面是ExpandableListView的常用方法说明:
setAdapter : 设置适配器。适配器类型为ExpandableListAdapter
expandGroup : 展开指定分组。
collapseGroup : 收起指定分组。
isGroupExpanded : 判断指定分组是否展开。
setSelectedGroup : 设置选中的分组。
setSelectedChild : 设置选中的子项。
setGroupIndicator : 设置指定分组的指示图像。
setChildIndicator : 设置指定子项的指示图像。
ExpandableListView监听器
除了OnItemClickListener,ExpandableListView新加了下面几个监听器:
1、分组展开事件,相关类名与方法说明如下:
监听器类名 : OnGroupExpandListener
设置监听器的方法 : setOnGroupExpandListener
监听器需要重写的点击方法 : onGroupExpand
2、分组收起事件,相关类名与方法说明如下:
监听器类名 : OnGroupCollapseListener
设置监听器的方法 : setOnGroupCollapseListener
监听器需要重写的点击方法 : onGroupCollapse
3、分组点击事件,相关类名与方法说明如下:
监听器类名 : OnGroupClickListener
设置监听器的方法 : setOnGroupClickListener
监听器需要重写的点击方法 : onGroupClick
4、子项点击事件,相关类名与方法说明如下:
监听器类名 : OnChildClickListener
设置监听器的方法 : setOnChildClickListener
监听器需要重写的点击方法 : onChildClick
ExpandableListView适配器
ExpandableListAdapter是ExpandableListView的专用适配器,它并不继承自其他适配器。
下面是ExpandableListAdapter经常要重写的几个方法:
getGroupCount : 获取分组的个数。
getChildrenCount : 获取子项的个数。
getGroupView : 获取指定分组的视图。
getChildView : 获取指定子项的视图。
isChildSelectable : 判断子项是否允许选择。
ExpandableListView常见问题
ExpandableListView有时会发现子项不会响应点击事件,这可能是某个环节没有正确设置。要让子项目响应点击事件,需满足下面三个条件:
1、ExpandableListAdapter适配器的isChildSelectable方法要返回true;
2、ExpandableListView对象要注册监听器setOnChildClickListener,并重写onChildClick方法;
3、子项目中若有Button、EditText等默认占用焦点的控件,要去除焦点占用,即setFocusable和setFocusableInTouchMode设置为false;
下面是ExpandableListView的一个应用例子效果截图(电子邮箱):
下面是运用ExpandableListView的代码示例:
适配器代码
import java.util.ArrayList; import com.example.exmfoldlist.R; import com.example.exmfoldlist.bean.MailBox; import com.example.exmfoldlist.bean.MailItem; import android.content.Context; import android.database.DataSetObserver; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.ExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ExpandableListView.OnGroupClickListener; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; public class CustomExpandAdapter implements ExpandableListAdapter,OnGroupClickListener,OnChildClickListener { private final static String TAG = "CustomExpandAdapter"; private LayoutInflater mInflater; private Context mContext; private ArrayList<MailBox> mBoxList; public CustomExpandAdapter(Context context, ArrayList<MailBox> box_list) { mInflater = LayoutInflater.from(context); mContext = context; mBoxList = box_list; } @Override public void registerDataSetObserver(DataSetObserver observer) { } @Override public void unregisterDataSetObserver(DataSetObserver observer) { } @Override public int getGroupCount() { return mBoxList.size(); } @Override public int getChildrenCount(int groupPosition) { return mBoxList.get(groupPosition).mail_list.size(); } @Override public Object getGroup(int groupPosition) { return mBoxList.get(groupPosition); } @Override public Object getChild(int groupPosition, int childPosition) { return mBoxList.get(groupPosition).mail_list.get(childPosition); } @Override public long getGroupId(int groupPosition) { return groupPosition; } @Override public long getChildId(int groupPosition, int childPosition) { return childPosition; } @Override public boolean hasStableIds() { return false; } @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { ViewHolderBox holder = null; if (convertView == null) { holder = new ViewHolderBox(); convertView = mInflater.inflate(R.layout.list_box, null); holder.iv_box = (ImageView) convertView.findViewById(R.id.iv_box); holder.tv_box = (TextView) convertView.findViewById(R.id.tv_box); holder.tv_count = (TextView) convertView.findViewById(R.id.tv_count); convertView.setTag(holder); } else { holder = (ViewHolderBox) convertView.getTag(); } MailBox box = mBoxList.get(groupPosition); holder.iv_box.setImageResource(box.box_icon); holder.tv_box.setText(box.box_title); holder.tv_count.setText(box.mail_list.size()+"封邮件"); return convertView; } @Override public View getChildView(final int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { ViewHolderMail holder = null; if (convertView == null) { holder = new ViewHolderMail(); convertView = mInflater.inflate(R.layout.list_mail, null); holder.ck_mail = (CheckBox) convertView.findViewById(R.id.ck_mail); holder.tv_date = (TextView) convertView.findViewById(R.id.tv_date); convertView.setTag(holder); } else { holder = (ViewHolderMail) convertView.getTag(); } MailItem item = mBoxList.get(groupPosition).mail_list.get(childPosition); holder.ck_mail.setFocusable(false); holder.ck_mail.setFocusableInTouchMode(false); holder.ck_mail.setText(item.mail_title); holder.ck_mail.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { MailBox box = mBoxList.get(groupPosition); MailItem item = box.mail_list.get(childPosition); String desc = String.format("您点击了%s的邮件,标题是%s", box.box_title, item.mail_title); Toast.makeText(mContext, desc, Toast.LENGTH_LONG).show(); } }); holder.tv_date.setText(item.mail_date); return convertView; } //如果子条目需要响应点击事件,这里要返回true @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } @Override public boolean areAllItemsEnabled() { return true; } @Override public boolean isEmpty() { return false; } @Override public void onGroupExpanded(int groupPosition) { } @Override public void onGroupCollapsed(int groupPosition) { } @Override public long getCombinedChildId(long groupId, long childId) { return 0; } @Override public long getCombinedGroupId(long groupId) { return 0; } public final class ViewHolderBox { public ImageView iv_box; public TextView tv_box; public TextView tv_count; } public final class ViewHolderMail { public CheckBox ck_mail; public TextView tv_date; } @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { ViewHolderMail holder = (ViewHolderMail) v.getTag(); holder.ck_mail.setChecked(!(holder.ck_mail.isChecked())); return true; } //如果返回true,就不会展示子列表 @Override public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) { String desc = String.format("您点击了%s", mBoxList.get(groupPosition).box_title); Toast.makeText(mContext, desc, Toast.LENGTH_LONG).show(); return false; } }
调用的代码
import java.util.ArrayList; import com.example.exmfoldlist.adapter.CustomExpandAdapter; import com.example.exmfoldlist.bean.MailBox; import com.example.exmfoldlist.bean.MailItem; import android.app.Activity; import android.os.Bundle; import android.widget.ExpandableListView; public class ExpandActivity extends Activity { private final static String TAG = "ExpandActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_expand); ExpandableListView elv_list = (ExpandableListView) findViewById(R.id.elv_list); final ArrayList<MailBox> box_list = new ArrayList<MailBox>(); box_list.add(new MailBox(R.drawable.mail_folder_inbox, "收件箱", getRecvMail())); box_list.add(new MailBox(R.drawable.mail_folder_outbox, "发件箱", getSentMail())); box_list.add(new MailBox(R.drawable.mail_folder_draft, "草稿箱", getDraftMail())); box_list.add(new MailBox(R.drawable.mail_folder_recycle, "废件箱", getRecycleMail())); CustomExpandAdapter adapter = new CustomExpandAdapter(this, box_list); elv_list.setAdapter(adapter); elv_list.setOnChildClickListener(adapter); elv_list.setOnGroupClickListener(adapter); elv_list.expandGroup(0); //默认展开第一个邮件夹 } private ArrayList<MailItem> getRecvMail() { ArrayList<MailItem> mail_list = new ArrayList<MailItem>(); mail_list.add(new MailItem("这里是收件箱呀1", "2016年3月25日")); mail_list.add(new MailItem("这里是收件箱呀2", "2016年3月20日")); mail_list.add(new MailItem("这里是收件箱呀3", "2016年3月24日")); mail_list.add(new MailItem("这里是收件箱呀4", "2016年3月21日")); mail_list.add(new MailItem("这里是收件箱呀5", "2016年3月23日")); return mail_list; } private ArrayList<MailItem> getSentMail() { ArrayList<MailItem> mail_list = new ArrayList<MailItem>(); mail_list.add(new MailItem("邮件发出去了吗1", "2016年3月25日")); mail_list.add(new MailItem("邮件发出去了吗2", "2016年3月24日")); mail_list.add(new MailItem("邮件发出去了吗3", "2016年3月21日")); mail_list.add(new MailItem("邮件发出去了吗4", "2016年3月23日")); mail_list.add(new MailItem("邮件发出去了吗5", "2016年3月20日")); return mail_list; } private ArrayList<MailItem> getDraftMail() { ArrayList<MailItem> mail_list = new ArrayList<MailItem>(); mail_list.add(new MailItem("暂时放在草稿箱吧1", "2016年3月24日")); mail_list.add(new MailItem("暂时放在草稿箱吧2", "2016年3月21日")); mail_list.add(new MailItem("暂时放在草稿箱吧3", "2016年3月25日")); mail_list.add(new MailItem("暂时放在草稿箱吧4", "2016年3月20日")); mail_list.add(new MailItem("暂时放在草稿箱吧5", "2016年3月23日")); return mail_list; } private ArrayList<MailItem> getRecycleMail() { ArrayList<MailItem> mail_list = new ArrayList<MailItem>(); mail_list.add(new MailItem("啊啊啊,怎么被删除了1", "2016年3月21日")); mail_list.add(new MailItem("啊啊啊,怎么被删除了2", "2016年3月23日")); mail_list.add(new MailItem("啊啊啊,怎么被删除了3", "2016年3月25日")); mail_list.add(new MailItem("啊啊啊,怎么被删除了4", "2016年3月20日")); mail_list.add(new MailItem("啊啊啊,怎么被删除了5", "2016年3月24日")); return mail_list; } }
折叠式布局FoldingLayout
ExpandableListView对于一般场景的折叠式列表已经够用了,可是它的UI风格略显呆板,如果我们想来个显示特效,比如加上折叠展开的动画,那最好还是自己写个折叠式列表控件。
FoldingLayout便是这样一个开源的折叠式布局控件,它实现了像折纸那样折叠展开和折叠收起的动画。下面是FoldingLayout的常用方法说明:
setFoldFactor : 设置折叠的因子。0表示收起,1表示展开。
setOrientation : 设置折叠的方向。VERTICAL表示垂直方向,HORIZONTAL表示水平方向。
setNumberOfFolds : 设置折叠的页数。
FoldingLayout也提供了折叠事件的监听,相关类名与方法说明如下:
监听器类名 : OnFoldListener
设置监听器的方法 : setFoldListener
监听器需要重写的点击方法 :
onStartFold : 开始折叠时触发。
onFoldingState : 折叠状态变化时触发。
onEndFold : 结束折叠时触发。
下面是运用FoldingLayout的代码示例:
import com.example.exmfoldlist.util.MetricsUtil; import com.example.exmfoldlist.view.FoldingLayout; import com.example.exmfoldlist.view.OnFoldListener; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.view.View.OnClickListener; import android.view.animation.AccelerateInterpolator; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; public class FoldingActivity extends Activity { private String TAG_ARROW = "service_arrow"; private String TAG_ITEM = "service_item"; private View mBottomView; private LinearLayout mTrafficLayout, mLifeLayout, mMedicalLayout, mLiveLayout, mPublicLayout; private RelativeLayout mTrafficBarLayout, mLifeBarLayout, mMedicalBarLayout, mLiveBarLayout, mPublicBarLayout; private FoldingLayout mTrafficFoldingLayout, mLifeFoldingLayout, mMedicalFoldingLayout, mLiveFoldingLayout, mPublicFoldingLayout; private final int FOLD_ANIMATION_DURATION = 1000; private int mNumberOfFolds = 3; private Handler mHandler = new Handler(); protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_folding); mTrafficLayout = (LinearLayout) findViewById(R.id.traffic_layout); mLifeLayout = (LinearLayout) findViewById(R.id.life_layout); mMedicalLayout = (LinearLayout) findViewById(R.id.medical_layout); mLiveLayout = (LinearLayout) findViewById(R.id.live_layout); mPublicLayout = (LinearLayout) findViewById(R.id.public_layout); mTrafficBarLayout = (RelativeLayout) findViewById(R.id.traffic_bar_layout); mLifeBarLayout = (RelativeLayout) findViewById(R.id.life_bar_layout); mMedicalBarLayout = (RelativeLayout) findViewById(R.id.medical_bar_layout); mLiveBarLayout = (RelativeLayout) findViewById(R.id.live_bar_layout); mPublicBarLayout = (RelativeLayout) findViewById(R.id.public_bar_layout); mTrafficFoldingLayout = ((FoldingLayout) mTrafficLayout.findViewWithTag(TAG_ITEM)); mLifeFoldingLayout = ((FoldingLayout) mLifeLayout.findViewWithTag(TAG_ITEM)); mMedicalFoldingLayout = ((FoldingLayout) mMedicalLayout.findViewWithTag(TAG_ITEM)); mLiveFoldingLayout = ((FoldingLayout) mLiveLayout.findViewWithTag(TAG_ITEM)); mPublicFoldingLayout = ((FoldingLayout) mPublicLayout.findViewWithTag(TAG_ITEM)); mBottomView = findViewById(R.id.bottom_view); initFoldingLayout(mTrafficFoldingLayout, mTrafficBarLayout, mTrafficLayout, mLifeLayout); initFoldingLayout(mLifeFoldingLayout, mLifeBarLayout, mLifeLayout, mMedicalLayout); initFoldingLayout(mMedicalFoldingLayout, mMedicalBarLayout, mMedicalLayout, mLiveLayout); initFoldingLayout(mLiveFoldingLayout, mLiveBarLayout, mLiveLayout, mPublicLayout); initFoldingLayout(mPublicFoldingLayout, mPublicBarLayout, mPublicLayout, mBottomView); setBarEnabled(false); mHandler.postDelayed(mDefaultFold, 150); } private void initFoldingLayout(final FoldingLayout foldingLayout, View bar, final View thisView, final View nextView) { foldingLayout.setVisibility(View.INVISIBLE); bar.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { handleAnimation(v, foldingLayout, thisView, nextView); } }); foldingLayout.setNumberOfFolds(mNumberOfFolds); animateFold(foldingLayout, 100); setMarginToTop(1, nextView); } private void setBarEnabled(boolean enabled) { mTrafficBarLayout.setEnabled(enabled); mLifeBarLayout.setEnabled(enabled); mMedicalBarLayout.setEnabled(enabled); mLiveBarLayout.setEnabled(enabled); mPublicBarLayout.setEnabled(enabled); mTrafficFoldingLayout.setFoldFactor(!enabled?0.0f:1.0f); mLifeFoldingLayout.setFoldFactor(!enabled?0.0f:1.0f); mMedicalFoldingLayout.setFoldFactor(!enabled?0.0f:1.0f); mLiveFoldingLayout.setFoldFactor(!enabled?0.0f:1.0f); mPublicFoldingLayout.setFoldFactor(!enabled?0.0f:1.0f); } private Runnable mDefaultFold = new Runnable() { @Override public void run() { setBarEnabled(true); handleAnimation(mTrafficBarLayout, mTrafficFoldingLayout, mTrafficLayout, mLifeLayout); } }; private void handleAnimation(final View bar, final FoldingLayout foldingLayout, View parent, final View nextParent) { foldingLayout.setVisibility(View.VISIBLE); final ImageView arrow = (ImageView) parent.findViewWithTag(TAG_ARROW); foldingLayout.setFoldListener(new OnFoldListener() { @Override public void onStartFold(float foldFactor) { bar.setClickable(true); arrow.setBackgroundResource(R.drawable.service_arrow_up); resetMarginToTop(foldingLayout, foldFactor, nextParent); } @Override public void onFoldingState(float foldFactor, float foldDrawHeight) { bar.setClickable(false); resetMarginToTop(foldingLayout, foldFactor, nextParent); } @Override public void onEndFold(float foldFactor) { bar.setClickable(true); arrow.setBackgroundResource(R.drawable.service_arrow_down); resetMarginToTop(foldingLayout, foldFactor, nextParent); } }); animateFold(foldingLayout, FOLD_ANIMATION_DURATION); } private void resetMarginToTop(View view, float foldFactor, View nextParent) { LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) nextParent.getLayoutParams(); lp.topMargin =(int)( - view.getMeasuredHeight() * foldFactor) + MetricsUtil.dip2px(FoldingActivity.this, 10); nextParent.setLayoutParams(lp); } private void setMarginToTop(float foldFactor, View nextParent) { LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) nextParent.getLayoutParams(); lp.topMargin =(int)( - MetricsUtil.dip2px(FoldingActivity.this, 135) * foldFactor) + MetricsUtil.dip2px(FoldingActivity.this, 10); nextParent.setLayoutParams(lp); } public void animateFold(FoldingLayout foldLayout, int duration) { float foldFactor = foldLayout.getFoldFactor(); ObjectAnimator animator = ObjectAnimator.ofFloat(foldLayout, "foldFactor", foldFactor, foldFactor > 0 ? 0 : 1); animator.setRepeatMode(ValueAnimator.REVERSE); animator.setRepeatCount(0); animator.setDuration(duration); animator.setInterpolator(new AccelerateInterpolator()); animator.start(); } }