模拟一些优秀的APP的界面绘制,实现类似功能。绘制“简陋版界面”,哈哈哈。
主要控件:TabLayout+RecyclerView+自定义CalendarView
模拟重点:TabLayout
https://download.csdn.net/download/Nicholas1hzf/12363547
1. 顶部日期选择器(30天)的实现
难点: 最右侧阴影效果的实现,Tab的标题获取,自定义日历选择器以及日历选择器与选项卡布局的交互
思路介绍:
外层布局使用 FrameLayout 是因为帧布局可以实现控件遮挡的效果,前面的控件可以被后面的控件遮挡。TabLayout 占满整个布局的宽度,再把“全部日期”的布局(LinearLayout)布置在父布局(FrameLayout)的最右侧(layout_gravity=“end”),成功将 Tab 挡住,这样当 TabLayout 的 Tab 向左滑动时,有一种“火车出洞”的感觉,为了增强此效果,最右侧的布局没有简单使用一个 TextView 而是采用线性布局,内含一个 View(背景使用深色20%透明【#33000000】,产生阴影遮挡效果)和一个底部带有向下箭头(drawableBottom="@drawable/ic_arrow_drop_down")的 TextView(文字提示,提供点击)
A.)最右侧阴影效果的实现
关键代码
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_color_shade">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp"
android:layout_gravity="start|center_vertical"
app:tabMode="scrollable"
app:tabGravity="fill"
app:tabIndicatorGravity="stretch"
app:tabIndicator="@drawable/bg_rrc_filled_white"
app:tabIndicatorColor="@color/background_color_white"
app:tabTextColor="@color/fc_white"
app:tabSelectedTextColor="@color/fc_light_blue"
app:tabTextAppearance="@style/AppTheme.TabLayout.TextAppearance.Date"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end">
<View
android:layout_width="10dp"
android:layout_height="match_parent"
android:background="@color/background_color_shade3"/>
<TextView
android:id="@+id/all_date_selector_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/background_color_light_blue"
android:text="@string/all_date"
android:textColor="@color/fc_white"
android:textSize="?attr/SmallText"
android:gravity="center"
android:drawableBottom="@drawable/ic_arrow_drop_down"/>
LinearLayout>
FrameLayout>
B.)Tab的标题获取
TabLayout-标签布局
注意点:tabIndicator 属性只提供样式,不提供颜色
属性名 | 属性值 | 效果 |
---|---|---|
tabMode | scrollable | Tab 可滚动 |
tabGravity | fill | 内容尽可能充满 TabLayout |
tabIndicatorGravity | stretch | 指示器拉伸填满 Tab |
tabIndicator | @drawable/bg_rrc_filled_white | 指示器样式 |
tabIndicatorColor | @color/background_color_white | 指示器颜色 |
tabTextColor | @color/fc_white | Tab 文字颜色 |
tabSelectedTextColor | @color/fc_light_blue | Tab 文字选中颜色 |
Java关键代码
/**********DateSelectorActivity**********/
//控件获取
TabLayout mTabLayout = findViewById(R.id.tab_layout);
//Tab 标题的获取
List<String> mResults = new ArrayList<>();
mResults.addAll(DateUtils.get30DD(Calendar.getInstance()));
mResults.add("敬请\n期待");
//TabLayout 标题绑定
for (String s : mResults) {
TabLayout.Tab tab = mTabLayout.newTab();
tab.setText(s);
mTabLayout.addTab(tab);
}
//给 TabLayout 的 item 添加分割线
LinearLayout linearLayout = (LinearLayout) mTabLayout.getChildAt(0);
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
linearLayout.setDividerDrawable(ContextCompat.getDrawable(this, R.drawable.tab_layout_cut_off_line));
linearLayout.setDividerPadding(12);
/**********DateUtils**********/
/**
* 获取某天之后30天的星期与日期 MM-dd\n周几
* @param calendar 某天
* @return List
*/
public static List<String> get30DD(Calendar calendar){
List<String> dd = new ArrayList<>();
for (int i = 0; i < 30; i++) {
String data = "";
switch (i){
case 0:
data = getFutureDate(i,calendar)+"\n今天";
break;
case 1:
data = getFutureDate(i,calendar)+"\n明天";
break;
case 2:
data = getFutureDate(i,calendar)+"\n后天";
break;
default:
data = getFutureDate(i,calendar)+"\n"+getFutureDay(i,calendar);
}
dd.add(data);
}
return dd;
}
//获取 calendar 未来某天的月日
public static String getFutureDate(int future,Calendar calendar){
Calendar calendar1 = new GregorianCalendar();
calendar1.setTime(calendar.getTime());
calendar1.set(Calendar.DAY_OF_YEAR, calendar.get(Calendar.DAY_OF_YEAR) + future);
Date today = calendar1.getTime();
SimpleDateFormat format = new SimpleDateFormat("MM-dd", Locale.CHINA);
return format.format(today);
}
//获取 calendar 未来某天的星期
public static String getFutureDay(int future,Calendar calendar){
Calendar calendar1 = new GregorianCalendar();
calendar1.setTime(calendar.getTime());
calendar1.set(Calendar.DAY_OF_YEAR, calendar.get(Calendar.DAY_OF_YEAR) + future);
Date today = calendar1.getTime();
SimpleDateFormat format = new SimpleDateFormat("EEEE", Locale.CHINA);
return format.format(today);
}
C.)自定义日历选择器
学习改编自 android自定义控件之日历控件的实现
/**********CalendarView**********/
private void initial() {
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); //获取今天周几 周日为1 周六为7
int monthStart = -1;
if(dayOfWeek >= 2 && dayOfWeek <= 7){ //如果周一到周六
monthStart = dayOfWeek - 2; //本月第一天为0-5
}else if(dayOfWeek == 1){ //如果是周日
monthStart = 6; //本月第一天为6
}
curStartIndex = monthStart; //本月第一天的索引位置为monthStart(0-6)
date[monthStart] = 1;//将本月第一天以1号存入
int daysOfMonth = daysOfCurrentMonth();//获取本月天数
for (int i = 1; i < daysOfMonth; i++) { //将这个月的日期存入
date[monthStart + i] = i + 1;
}
curEndIndex = monthStart + daysOfMonth;//本月最后一天的索引位置为monthStart(0-6)+天数
if(calendar.get(Calendar.YEAR) == Calendar.getInstance().get(Calendar.YEAR)
&& calendar.get(Calendar.MONTH) == Calendar.getInstance().get(Calendar.MONTH)){
todayIndex = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + monthStart - 1;
}else{
todayIndex = -1;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float lineSX = cellWidth/2;
float lineEX = cellWidth*6+cellWidth/2;
float lineY = cellHeight*0.9f;
Paint linePaint = RenderUtil.getPaint(Color.parseColor("#92A7C2"));
linePaint.setStrokeWidth(4);
//绘制星期
float baseline = RenderUtil.getBaseline(0, cellHeight, weekTextPaint);
for (int i = 0; i < 7; i++) {
float weekTextX = RenderUtil.getStartX(cellWidth * i + cellWidth * 0.5f, weekTextPaint, weekText[i]);
canvas.drawText(weekText[i], weekTextX, baseline, weekTextPaint);
}
//添加 星期下面的线
canvas.drawLine(lineSX,lineY,lineEX,lineY,linePaint);
//绘制日期
for (int i = curStartIndex; i < curEndIndex; i++) {
if (i == todayIndex && i == selectedIndex){
drawCircle(canvas, i, selectedDayBgPaint, cellHeight * 0.48f);
drawText(canvas, i, selectedDayTextPaint, "" + date[i]);
} else if(i == todayIndex){
drawText(canvas, i, todayTextPaint, "" + date[i]);
}else if(i == selectedIndex && i > todayIndex){
drawCircle(canvas, i, selectedDayBgPaint, cellHeight * 0.48f);
drawText(canvas, i, selectedDayTextPaint, "" + date[i]);
}else if (i < todayIndex){
drawText(canvas, i, beforeTodayTextPaint, "" + date[i]);
} else{
drawText(canvas, i, textPaint, "" + date[i]);
}
}
}
//绘制文字函数
private void drawText(Canvas canvas, int index, Paint paint, String text) {
if(isIllegalIndex(index)){
return;
}
int x = getXByIndex(index);
int y = getYByIndex(index);
float top = cellHeight + (y - 1) * cellHeight;
float bottom = top + cellHeight;
float baseline = RenderUtil.getBaseline(top, bottom, paint);
float startX = RenderUtil.getStartX(cellWidth * (x - 1) + cellWidth * 0.5f, paint, text);
canvas.drawText(text, startX, baseline, paint);
}
//绘制选中圆样式
private void drawCircle(Canvas canvas, int index, Paint paint, float radius){
if(isIllegalIndex(index)){
return;
}
int x = getXByIndex(index);
int y = getYByIndex(index);
float centreY = cellHeight + (y - 1) * cellHeight + cellHeight * 0.5f;
float centreX = cellWidth * (x - 1) + cellWidth * 0.5f;
canvas.drawCircle(centreX, centreY, radius, paint);
}
/**********DateSelectorActivity**********/
//点击全部日期弹出日历选择器界面
//TextView 点击事件监听
mTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showPop(v);
}
});
//弹窗函数 showPop(View v);
private void showPop(View view){
//获取弹窗布局
View popView = View.inflate(this, R.layout.pw_select_date, null);
//实例化布局中的控件
mCalendarView = popView.findViewById(R.id.calendar_view);
final TextView thisMonthTV = popView.findViewById(R.id.this_month_text_view);
final TextView nextMonthTV = popView.findViewById(R.id.next_month_text_view);
ImageView ensureIV = popView.findViewById(R.id.select_date_image_view);
//获得弹窗,并进行基础设置
final PopupWindow popupWindow = new PopupWindow(getResources().getDisplayMetrics().widthPixels-120,getResources().getDisplayMetrics().heightPixels/2);
popupWindow.setContentView(popView);
popupWindow.setOutsideTouchable(true);
popupWindow.setFocusable(true);
popupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
addBackground(popupWindow);
popupWindow.showAtLocation(view, Gravity.CENTER,0,0);
//初始化数据(获取当前月份和下一个月的月份)
final int year = Calendar.getInstance().get(Calendar.YEAR);
final int month = Calendar.getInstance().get(Calendar.MONTH)+1;
int nextMonth;
int nextYear = year;
//考虑下一个月跨年的情况
if (month == 12){
nextMonth = 1;
nextYear++;
}else {
nextMonth = month+1;
}
//月份显示格式为:04月,11月,小于10的月份加0
String thisMonthString = "";
if (month < 10){
thisMonthString = "0"+month+"月";
}else {
thisMonthString = month+"月";
}
String nextMonthString = "";
if (nextMonth < 10){
nextMonthString = "0"+nextMonth+"月";
}else {
nextMonthString = nextMonth+"月";
}
thisMonthTV.setText(thisMonthString);
nextMonthTV.setText(nextMonthString);
//日历控件点击事件监听,下文会详细介绍
mCalendarView.setOnItemClickListener(new ICalendarView.OnItemClickListener() {
@Override
public void onItemClick(int day) {
int today = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
int month = Calendar.getInstance().get(Calendar.MONTH)+1;
if (day >= today || month != mCalendarView.getMonth()){
selectDate = day;
}
}
});
//确认图标点击事件监听,下文会详细介绍
ensureIV.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String select = DateUtils.getCalendarStringByYearMonthDate(mCalendarView.getMonth(),selectDate);
int index = getTabSelectedIndex(select);
mTabLayout.getTabAt(index).select();
popupWindow.dismiss();
}
});
//本月按钮,更新背景及日历(本月按钮和下一个月按钮可使用 TabLayout 替代)
thisMonthTV.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mCalendarView.refresh0(year,month);
thisMonthTV.setTextColor(getResources().getColor(R.color.fc_white));
nextMonthTV.setTextColor(getResources().getColor(R.color.colorPrimaryDark));
thisMonthTV.setBackground(getResources().getDrawable(R.drawable.bg_rrc_filled_primary_dark_radius25_left_right));
nextMonthTV.setBackground(null);
}
});
//下一个月按钮,更新背景及日历(本月按钮和下一个月按钮可使用 TabLayout 替代)
final int finalNextYear = nextYear;
final int finalNextMonth = nextMonth;
nextMonthTV.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mCalendarView.refresh0(finalNextYear, finalNextMonth);
nextMonthTV.setTextColor(getResources().getColor(R.color.fc_white));
thisMonthTV.setTextColor(getResources().getColor(R.color.colorPrimaryDark));
nextMonthTV.setBackground(getResources().getDrawable(R.drawable.bg_rrc_filled_primary_dark_radius25_left_right));
thisMonthTV.setBackground(null);
}
});
}
D.)日历选择器与选项卡布局的交互
/**********DateSelectorActivity**********/
//日历控件点击事件监听
mCalendarView.setOnItemClickListener(new ICalendarView.OnItemClickListener() {
@Override
public void onItemClick(int day) {
//获取今天的月份及日期
int today = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
int month = Calendar.getInstance().get(Calendar.MONTH)+1;//月份为0-11所以+1
//今日之前的日子,点击无效
if (day >= today || month != mCalendarView.getMonth()){
selectDate = day;
}
}
});
//确认图标点击事件监听
ensureIV.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String select = DateUtils.getCalendarStringByYearMonthDate(mCalendarView.getMonth(),selectDate);
//根据点击的日期获取相应的 TabLayout 中 Tab 索引
int index = getTabSelectedIndex(select);
//根据索引设置相应的 Tab 为选中状态
mTabLayout.getTabAt(index).select();
popupWindow.dismiss();
}
});
//根据点击的日期获取相应的 TabLayout 中 Tab 索引
private int getTabSelectedIndex(String content) {
for (int i = 0; i < mResults.size()-1; i++) {
//mResults中的格式为“04-26\n星期日”,所以获取子串“04-26”进行匹配
if (mResults.get(i).substring(0,5).equals(content)){
return i;
}
}
//超过30天的日期获取最后一个Tab的索引
return mResults.size()-1;
}
2. 车票列表RecyclerView的实现
RecyclerView学习
难点: RecyclerView 适配器,item 刷新,item 数据传输以及和 TabLayout 交互
思路介绍:
根据车票信息制作相应的实体类 bean:Ticket.class 采用相对布局(RelativeLayout)合理放置车票信息。根据布局及实体类编写 Adapter,绑定 RecyclerView 并完成相应点击事件的监听,实现点击不同的 item,详细页面展示不同的信息。在 Adapter 中编写数据刷新函数,配合 TabLayout 的 Tab 选择完成相应交互。
A.)RecyclerView适配器
public class TicketAdapterRV extends RecyclerView.Adapter<TicketAdapterRV.ViewHolder> {
//实体类集合
private List<Ticket> mTickets;
//上下文
private Context mContext;
//静态内部类
static class ViewHolder extends RecyclerView.ViewHolder{
private Ticket mTicket;
private TextView departureTimeTV;
private TextView originStationTV;
private TextView destinationStationTV;
private TextView busTypeTV;
private TextView ticketPriceTV;
private TextView ticketNumTV;
public ViewHolder (View view){
super(view);
departureTimeTV = view.findViewById(R.id.departure_time);
originStationTV = view.findViewById(R.id.origin_station);
destinationStationTV = view.findViewById(R.id.destination_station);
busTypeTV = view.findViewById(R.id.bus_type);
ticketPriceTV = view.findViewById(R.id.ticket_price);
ticketNumTV = view.findViewById(R.id.ticket_num);
}
//绑定数据
public void bindTicket(Ticket ticket){
mTicket = ticket;
String ticketPrice = ticket.getTicketPrice()+"";
String ticketNum = "余票"+ticket.getTicketNum()+"张";
departureTimeTV.setText(ticket.getDepartureTime());
originStationTV.setText(ticket.getOriginStation());
destinationStationTV.setText(ticket.getDestinationStation());
busTypeTV.setText(ticket.getBusType());
ticketPriceTV.setText(ticketPrice);
ticketNumTV.setText(ticketNum);
}
}
//构造函数
public TicketAdapterRV(List<Ticket> tickets,Context context){
mTickets = new ArrayList<>();
mTickets.addAll(tickets);
mContext = context;
}
//用于告诉 RecyclerView 一共有多少子项,直接返回数据源的长度即可
@Override
public int getItemCount() {
return mTickets.size();
}
//用于对 RecyclerView 子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行。
@Override
public void onBindViewHolder(TicketAdapterRV.ViewHolder holder, int position) {
final Ticket ticket = mTickets.get(position);
holder.bindTicket(ticket);
//设置点击事件的回调
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null){
listener.onClick(ticket);
}
}
});
}
//用于创建 ViewHolder 实例
@Override
public TicketAdapterRV.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.rv_item_ticket,parent,false);
TicketAdapterRV.ViewHolder holder = new TicketAdapterRV.ViewHolder(view);
return holder;
}
//点击事件
//第一步 定义接口
public interface OnItemClickListener {
void onClick(Ticket ticket);
}
private OnItemClickListener listener;
//第二步, 写一个公共的方法
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
//刷新数据函数,在 Adapter 中对数据进行操作,可以有效防止因内存地址问题造成数据不刷新的 BUG
public void refreshData(List<Ticket> tickets){
mTickets.clear();
if (tickets != null){
mTickets.addAll(tickets);
}
notifyDataSetChanged();
}
}
B.)item刷新
/**********TicketAdapterRV**********/
//刷新数据函数,在 Adapter 中对数据进行操作,可以有效防止因内存地址问题造成数据不刷新的 BUG
public void refreshData(List<Ticket> tickets){
//清空原本的数据集合中的数据
mTickets.clear();
//若新数据不为空,则将其全部加入
if (tickets != null){
mTickets.addAll(tickets);
}
//通知 RecyclerView 数据集合已经发生改变
notifyDataSetChanged();
}
/**********DateSelectorActivity**********/
//适配器点击事件的回调函数,编写回调函数见上文 A.)RecyclerView 适配器
mAdapter.setOnItemClickListener(new TicketAdapterRV.OnItemClickListener() {
@Override
public void onClick(Ticket ticket) {
Intent intent = new Intent(DateSelectorActivity.this,TicketDetailActivity.class);
//跳转到详细页面,并将该位置的 item 所绑定的Ticket传递过去
//记得先把该实体类序列化,若采用 Serializable 序列化,则让Ticket.class 实现 Serializable 接口即可
intent.putExtra("TICKET", ticket);
startActivity(intent);
}
});
/**********TicketDetailActivity**********/
public class TicketDetailActivity extends AppCompatActivity {
public static final String TICKET = "TICKET";
private Ticket mTicket;
private TextView departureTimeTV;
private TextView originStationTV;
private TextView destinationStationTV;
private TextView busTypeTV;
private TextView ticketPriceTV;
private TextView ticketNumTV;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ticket_detail);
//初始化数据
initData();
//实例化控件
initElement();
//绑定数据并完成相应初始化工作
init();
}
private void init() {
String ticketPrice = mTicket.getTicketPrice()+"";
String ticketNum = "余票"+mTicket.getTicketNum()+"张";
departureTimeTV.setText(mTicket.getDepartureTime());
originStationTV.setText(mTicket.getOriginStation());
destinationStationTV.setText(mTicket.getDestinationStation());
busTypeTV.setText(mTicket.getBusType());
ticketPriceTV.setText(ticketPrice);
ticketNumTV.setText(ticketNum);
}
private void initElement() {
departureTimeTV = findViewById(R.id.departure_time_tv);
originStationTV = findViewById(R.id.origin_station_tv);
destinationStationTV = findViewById(R.id.destination_station_tv);
busTypeTV = findViewById(R.id.bus_type_tv);
ticketPriceTV = findViewById(R.id.ticket_price_tv);
ticketNumTV = findViewById(R.id.ticket_num_tv);
}
private void initData() {
//获取传递过来的序列化对象 Ticket
mTicket = (Ticket) getIntent().getSerializableExtra(TICKET);
}
}
D.)和TabLayout交互
/**********DateSelectorActivity**********/
//给 TabLayout 添加选择监听器
mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
//当选中某一 item 时,调用 RecyclerView 的适配器中的刷新函数,进行相应的数据刷新,这里仅切换三种集合,模拟刷新
int index = tab.getPosition()%3;
if (tab.getPosition() == mResults.size()-1){
index = -1;
}
switch (index){
case 0:
mAdapter.refreshData(mTickets1);
break;
case 1:
mAdapter.refreshData(mTickets2);
break;
case 2:
mAdapter.refreshData(mTickets3);
break;
default:
mAdapter.refreshData(null);
break;
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {}
});
多学习,多编码,多思考。实现后想想有没有更好的实现方法!与君共勉,一同进步!
持续学习Android中,如有错误请批评指正!