Android学习 UI模仿练习之“巴士管家”选取车票

模拟一些优秀的APP的界面绘制,实现类似功能。绘制“简陋版界面”,哈哈哈。

主要控件:TabLayout+RecyclerView+自定义CalendarView
模拟重点:TabLayout

一、界面效果

二、设计实现

(一)文件列表

Android学习 UI模仿练习之“巴士管家”选取车票_第1张图片

(二)0积分获取代码

https://download.csdn.net/download/Nicholas1hzf/12363547

(三)关键代码讲解

1. 顶部日期选择器(30天)的实现
Android学习 UI模仿练习之“巴士管家”选取车票_第2张图片
难点: 最右侧阴影效果的实现,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自定义控件之日历控件的实现
Android学习 UI模仿练习之“巴士管家”选取车票_第3张图片

/**********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学习
Android学习 UI模仿练习之“巴士管家”选取车票_第4张图片
难点: 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();
}

C.)item数据传输
Android学习 UI模仿练习之“巴士管家”选取车票_第5张图片

/**********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中,如有错误请批评指正!

你可能感兴趣的:(Android,学习)