Loading...
Searching...
No Matches
FloorSelectorView.java
Go to the documentation of this file.
1package com.navigine.view.widgets;
2
3import android.content.Context;
4import android.graphics.Canvas;
5import android.graphics.Color;
6import android.graphics.Paint;
7import android.graphics.Typeface;
8import android.graphics.drawable.GradientDrawable;
9import android.view.Gravity;
10import android.view.View;
11import android.view.ViewGroup;
12import android.widget.Button;
13import android.widget.FrameLayout;
14import android.widget.TextView;
15
16import androidx.annotation.NonNull;
17import androidx.annotation.Nullable;
18import androidx.core.view.ViewCompat;
19
20import com.navigine.view.widgets.FloorSelectorViewConfig;
21import androidx.recyclerview.widget.LinearLayoutManager;
22import androidx.recyclerview.widget.RecyclerView;
23
24import java.util.ArrayList;
25import java.util.List;
26
36public class FloorSelectorView extends FrameLayout {
37
40 void onSublocationSelected(int sublocationId);
41 }
42
43
44 private List<LevelInfo> floors = new ArrayList<>();
45 private int selectedFloorIndex = -1;
46
47 private RecyclerView recyclerView;
48 private LinearLayoutManager layoutManager;
49 private FloorAdapter adapter;
50
51 private Button topButton;
52 private Button bottomButton;
53
54 private OnSublocationSelectedListener onSublocationSelectedListener;
55 private FloorSelectorViewConfig config;
56
58 public FloorSelectorView(@NonNull Context context, OnSublocationSelectedListener onSublocationSelected) {
59 this(context, onSublocationSelected, null);
60 }
61
63 public FloorSelectorView(@NonNull Context context, OnSublocationSelectedListener onSublocationSelected,
64 @Nullable FloorSelectorViewConfig config) {
65 super(context);
66 this.onSublocationSelectedListener = onSublocationSelected;
67 this.config = config;
68 init();
69 }
70
71 private void init() {
72 setBackgroundColor(Color.TRANSPARENT);
73 setClipChildren(false);
74 setClipToPadding(false);
75
76 GradientDrawable bg = new GradientDrawable();
77 float radius = dpToPx((int) WidgetStyles.BORDER_RADIUS);
78 bg.setCornerRadius(radius);
79 bg.setColor(Color.WHITE);
80 setBackground(bg);
81 ViewCompat.setElevation(this, dpToPx((int) WidgetStyles.FLOOR_SELECTOR_SHADOW_ELEVATION));
82 setClipToOutline(true);
83
84 // RecyclerView
85 recyclerView = new RecyclerView(getContext());
86 recyclerView.setHasFixedSize(true);
87 layoutManager = new LinearLayoutManager(getContext());
88 recyclerView.setLayoutManager(layoutManager);
89 adapter = new FloorAdapter();
90 recyclerView.setAdapter(adapter);
91 recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
92 recyclerView.setScrollbarFadingEnabled(true);
93 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
94 @Override
95 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
96 updateButtonVisibility();
97 }
98 });
99
100 recyclerView.addItemDecoration(new DividerDecoration());
101
102 addView(recyclerView);
103
104 topButton = createArrowButton("▲",true);
105 bottomButton = createArrowButton("▼", false);
106
107 topButton.setOnClickListener(v -> scrollUp());
108 bottomButton.setOnClickListener(v -> scrollDown());
109
110 topButton.setVisibility(GONE);
111 bottomButton.setVisibility(GONE);
112
113 addView(topButton);
114 addView(bottomButton);
115 }
116
117 private Button createArrowButton(String text, boolean isTop) {
118 Button button = new Button(getContext());
119 button.setText(text);
120 button.setTextSize(WidgetStyles.SCROLL_BUTTON_FONT_SIZE);
121 button.setTextColor(WidgetStyles.BASE_BLACK);
122 button.setTypeface(null, Typeface.BOLD);
123 button.setAllCaps(false);
124
125 GradientDrawable drawable = new GradientDrawable();
126 float radius = dpToPx((int) WidgetStyles.BORDER_RADIUS);
127 if (isTop) {
128 drawable.setCornerRadii(new float[]{
129 radius, radius,
130 radius, radius,
131 0f, 0f,
132 0f, 0f
133 });
134 } else {
135 drawable.setCornerRadii(new float[]{
136 0f, 0f,
137 0f, 0f,
138 radius, radius,
139 radius, radius
140 });
141 }
142
143 drawable.setColor(WidgetStyles.BUTTON_BACKGROUND_COLOR);
144 button.setBackground(drawable);
145
146 LayoutParams lp = new LayoutParams(
147 ViewGroup.LayoutParams.MATCH_PARENT,
148 ViewGroup.LayoutParams.WRAP_CONTENT
149 );
150 lp.gravity = isTop ? Gravity.TOP : Gravity.BOTTOM;
151 button.setLayoutParams(lp);
152
153 return button;
154 }
155
157 public int getFloorsCount() {
158 return floors.size();
159 }
160
162 public void applyConfig(@Nullable FloorSelectorViewConfig newConfig) {
163 config = newConfig;
164 if (adapter != null) {
165 adapter.notifyDataSetChanged();
166 }
167 }
168
172 public void setFloors(List<LevelInfo> newFloors) {
173 floors.clear();
174 if (newFloors != null) floors.addAll(newFloors);
175
176 if (selectedFloorIndex < 0 || selectedFloorIndex >= this.floors.size()) {
177 selectedFloorIndex = this.floors.isEmpty() ? -1 : 0;
178 }
179 updateFrame();
180 adapter.notifyDataSetChanged();
181 recyclerView.scrollToPosition(selectedFloorIndex >= 0 ? selectedFloorIndex : 0);
182 updateButtonVisibility();
183 }
184
188 public void setSublocationId(int sublocationId) {
189 int newSelectedIndex = -1;
190
191 for (int i = 0; i < floors.size(); i++) {
192 if (floors.get(i).getSublocationId() == sublocationId) {
193 newSelectedIndex = i;
194 break;
195 }
196 }
197
198 if (newSelectedIndex == -1) {
199 return;
200 }
201
202 if (selectedFloorIndex == newSelectedIndex) {
203 adapter.notifyDataSetChanged();
204 updateButtonVisibility();
205 return;
206 }
207
208 selectedFloorIndex = newSelectedIndex;
209 adapter.notifyDataSetChanged();
210
211 if (floors.size() > WidgetStyles.MAX_VISIBLE_FLOORS) {
212 recyclerView.smoothScrollToPosition(selectedFloorIndex);
213 } else {
214 layoutManager.scrollToPositionWithOffset(selectedFloorIndex, 0);
215 }
216
217 recyclerView.post(this::updateButtonVisibility);
218 }
219
220 private void updateFrame() {
221 if (floors.isEmpty() || floors.size() == 1) {
222 setVisibility(GONE);
223 } else {
224 setVisibility(VISIBLE);
225 }
226 requestLayout();
227 post(this::updateButtonPositions);
228 }
229
230 private int dpToPx(float dp) {
231 return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
232 }
233
234 @Override
235 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
236
237 int rowHeight = dpToPx((int) WidgetStyles.FLOOR_ROW_HEIGHT);
238 int count = floors.size();
239
240 int desiredWidth = dpToPx((int) WidgetStyles.FLOOR_SELECTOR_WIDTH);
241 int desiredHeight;
242
243
244 if (count <= 1) {
245 desiredHeight = 0;
246 } else if (count < WidgetStyles.MAX_VISIBLE_FLOORS) {
247 desiredHeight = rowHeight * count;
248 } else {
249 desiredHeight = rowHeight * WidgetStyles.MAX_VISIBLE_FLOORS;
250 }
251
252 int width = resolveSize(desiredWidth, widthMeasureSpec);
253 int height = resolveSize(desiredHeight, heightMeasureSpec);
254
255 setMeasuredDimension(width, height);
256
257 int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
258 int childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
259
260 recyclerView.measure(childWidthSpec, childHeightSpec);
261
262 int arrowHeightSpec = MeasureSpec.makeMeasureSpec(rowHeight, MeasureSpec.EXACTLY);
263 topButton.measure(childWidthSpec, arrowHeightSpec);
264 bottomButton.measure(childWidthSpec, arrowHeightSpec);
265 }
266
267 @Override
268 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
269 int width = right - left;
270 int height = bottom - top;
271
272 if (floors.isEmpty() || floors.size() == 1) {
273 return;
274 }
275
276 recyclerView.layout(0, 0, width, height);
277
278 int rowHeight = dpToPx((int) WidgetStyles.FLOOR_ROW_HEIGHT);
279 topButton.layout(0, 0, width, rowHeight);
280 bottomButton.layout(0, height - rowHeight, width, height);
281 }
282
283 private void updateButtonPositions() {
284 if (getHeight() <= 0) return;
285 int rowHeight = dpToPx((int) WidgetStyles.FLOOR_ROW_HEIGHT);
286 topButton.layout(0, 0, getWidth(), rowHeight);
287 bottomButton.layout(0, getHeight() - rowHeight, getWidth(), getHeight());
288 }
289
290 private void updateButtonVisibility() {
291 if (floors.size() <= 1 || floors.size() <= WidgetStyles.MAX_VISIBLE_FLOORS ) {
292 topButton.setVisibility(GONE);
293 bottomButton.setVisibility(GONE);
294 return;
295 }
296
297 int firstVisible = layoutManager.findFirstCompletelyVisibleItemPosition();
298 int lastVisible = layoutManager.findLastCompletelyVisibleItemPosition();
299
300 if (firstVisible == RecyclerView.NO_POSITION || lastVisible == RecyclerView.NO_POSITION) {
301 topButton.setVisibility(GONE);
302 bottomButton.setVisibility(GONE);
303 return;
304 }
305
306 boolean canScrollUp = firstVisible > 0;
307 boolean canScrollDown = lastVisible < floors.size() - 1;
308
309 topButton.setVisibility(canScrollUp ? VISIBLE : GONE);
310 bottomButton.setVisibility(canScrollDown ? VISIBLE : GONE);
311 }
312
313 private void scrollUp() {
314 int firstVisible = layoutManager.findFirstVisibleItemPosition();
315 if (firstVisible == RecyclerView.NO_POSITION) return;
316 int target = Math.max(0, firstVisible - WidgetStyles.SCROLL_ITEMS_COUNT);
317 recyclerView.smoothScrollToPosition(target);
318 }
319
320 private void scrollDown() {
321 int lastVisible = layoutManager.findLastVisibleItemPosition();
322 if (lastVisible == RecyclerView.NO_POSITION) return;
323 int target = Math.min(floors.size() - 1, lastVisible + WidgetStyles.SCROLL_ITEMS_COUNT);
324 recyclerView.smoothScrollToPosition(target);
325 }
326
327 private class FloorAdapter extends RecyclerView.Adapter<FloorAdapter.ViewHolder> {
328 @NonNull
329 @Override
330 public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
331 TextView textView = new TextView(getContext());
332 textView.setGravity(Gravity.CENTER);
333 textView.setTextSize(WidgetStyles.FLOOR_SELECTOR_FONT_SIZE);
334 int horizontalPadding = dpToPx((int) WidgetStyles.FLOOR_SELECTOR_HORIZONTAL_PADDING);
335 int verticalPadding = dpToPx((int) WidgetStyles.FLOOR_SELECTOR_VERTICAL_PADDING);
336 textView.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
337
338 RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(
339 ViewGroup.LayoutParams.MATCH_PARENT,
340 dpToPx((int) WidgetStyles.FLOOR_ROW_HEIGHT)
341 );
342 textView.setLayoutParams(lp);
343
344 return new ViewHolder(textView);
345 }
346
347 @Override
348 public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
349 LevelInfo info = floors.get(position);
350 String displayText = truncateLevelId(info.getLevelId());
351 holder.textView.setText(displayText);
352
353 boolean isSelected = (position == selectedFloorIndex);
354 FloorSelectorViewConfig c = config != null ? config : FloorSelectorViewConfig.defaultConfig();
355 int accentColor = c.getAccentColor() != null ? c.getAccentColor() : WidgetStyles.BASE_BLUE;
356 int textColor = c.getTextColor() != null ? c.getTextColor() : WidgetStyles.BASE_BLACK;
357 holder.textView.setBackgroundColor(isSelected ? accentColor : Color.WHITE);
358 holder.textView.setTextColor(isSelected ? Color.WHITE : textColor);
359 }
360
361 @Override
362 public int getItemCount() {
363 return floors.size();
364 }
365
366 class ViewHolder extends RecyclerView.ViewHolder {
367 TextView textView;
368
369 ViewHolder(@NonNull View itemView) {
370 super(itemView);
371 textView = (TextView) itemView;
372 textView.setOnClickListener(v -> {
373 int pos = getAdapterPosition();
374 if (pos == RecyclerView.NO_POSITION || pos == selectedFloorIndex) return;
375
376 selectedFloorIndex = pos;
377 notifyDataSetChanged();
378
379 LevelInfo info = floors.get(pos);
380 if (onSublocationSelectedListener != null) {
381 onSublocationSelectedListener.onSublocationSelected(info.getSublocationId());
382 }
383 });
384 }
385 }
386 }
387
388 private class DividerDecoration extends RecyclerView.ItemDecoration {
389
390 private final Paint paint = new Paint();
391 private final int heightPx;
392
393 DividerDecoration() {
394 paint.setAntiAlias(true);
395 paint.setColor(WidgetStyles.DIVIDER_COLOR);
396 heightPx = dpToPx((int) WidgetStyles.DIVIDER_HEIGHT);
397 }
398
399 @Override
400 public void onDrawOver(@NonNull Canvas c,
401 @NonNull RecyclerView parent,
402 @NonNull RecyclerView.State state) {
403 int childCount = parent.getChildCount();
404 int left = 0;
405 int right = parent.getWidth();
406
407 for (int i = 0; i < childCount - 1; i++) {
408 View child = parent.getChildAt(i);
409 float top = child.getBottom();
410 float bottom = top + heightPx;
411 c.drawRect(left, top, right, bottom, paint);
412 }
413 }
414 }
415 private String truncateLevelId(String levelId) {
416 if (levelId == null) return "";
417 if (levelId.length() <= 5) return levelId;
418 return levelId.substring(0, 5) + "...";
419 }
420
424 public static class LevelInfo {
425 private final String levelId;
426 private final int sublocationId;
427
433 public LevelInfo(String levelId, int sublocationId) {
434 this.levelId = levelId;
435 this.sublocationId = sublocationId;
436 }
437
441 public String getLevelId() {
442 return levelId;
443 }
444
448 public int getSublocationId() {
449 return sublocationId;
450 }
451 }
452}