Android实现小米相机底部滑动指示器

Lillian ·
更新时间:2024-09-20
· 1972 次阅读

近期工作内容需要涉及到相机开发,其中一个功能点就是实现一个相机预览页底部的滑动指示器,现在整理出来供大家讨论参考。

先上一张图看下效果:

主要实现功能有:

1.支持左右滑动,每次滑动一个tab

2.支持tab点击,直接跳到对应tab

3.选中的tab一直处于居中位置

4.支持部分UI自定义(大家可根据需要自己改动)

5.tab点击回调

6.内置Tab接口,放入的内容需要实现Tab接口

7.设置预选中tab

public class CameraIndicator extends LinearLayout { // 当前选中的位置索引 private int currentIndex; //tabs集合 private Tab[] tabs; // 利用Scroller类实现最终的滑动效果 public Scroller mScroller; //滑动执行时间(ms) private int mDuration = 300; //选中text的颜色 private int selectedTextColor = 0xffffffff; //未选中的text的颜色 private int normalTextColor = 0xffffffff; //选中的text的背景 private Drawable selectedTextBackgroundDrawable; private int selectedTextBackgroundColor; private int selectedTextBackgroundResources; //是否正在滑动 private boolean isScrolling = false; private int onLayoutCount = 0; public CameraIndicator(Context context) { this(context, null); } public CameraIndicator(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public CameraIndicator(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScroller = new Scroller(context); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //测量所有子元素 measureChildren(widthMeasureSpec, heightMeasureSpec); //处理wrap_content的情况 int width = 0; int height = 0; if (getChildCount() == 0) { setMeasuredDimension(0, 0); } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); width += child.getMeasuredWidth(); height = Math.max(height, child.getMeasuredHeight()); } setMeasuredDimension(width, height); } else if (widthMode == MeasureSpec.AT_MOST) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); width += child.getMeasuredWidth(); } setMeasuredDimension(width, heightSize); } else if (heightMode == MeasureSpec.AT_MOST) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); height = Math.max(height, child.getMeasuredHeight()); } setMeasuredDimension(widthSize, height); } else { //如果自定义ViewGroup之初就已确认该ViewGroup宽高都是match_parent,那么直接设置即可 setMeasuredDimension(widthSize, heightSize); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //给选中text的添加背景会多次进入onLayout,会导致位置有问题,暂未解决 if (onLayoutCount > 0) { return; } onLayoutCount++; int counts = getChildCount(); int childLeft = 0; int childRight = 0; int childTop = 0; int childBottom = 0; //居中显示 int widthOffset = 0; //计算最左边的子view距离中心的距离 for (int i = 0; i < currentIndex; i++) { View childView = getChildAt(i); widthOffset += childView.getMeasuredWidth() + getMargins(childView).get(0)+getMargins(childView).get(2); } //计算出每个子view的位置 for (int i = 0; i < counts; i++) { View childView = getChildAt(i); childView.setOnClickListener(v -> moveTo(v)); if (i != 0) { View preView = getChildAt(i - 1); childLeft = preView.getRight() +getMargins(preView).get(2)+ getMargins(childView).get(0); } else { childLeft = (getWidth() - getChildAt(currentIndex).getMeasuredWidth()) / 2 - widthOffset; } childRight = childLeft + childView.getMeasuredWidth(); childTop = (getHeight() - childView.getMeasuredHeight()) / 2; childBottom = (getHeight() + childView.getMeasuredHeight()) / 2; childView.layout(childLeft, childTop, childRight, childBottom); } TextView indexText = (TextView) getChildAt(currentIndex); changeSelectedUIState(indexText); } private List<Integer> getMargins(View view) { LayoutParams params = (LayoutParams) view.getLayoutParams(); List<Integer> listMargin = new ArrayList<Integer>(); listMargin.add(params.leftMargin); listMargin.add(params.topMargin); listMargin.add(params.rightMargin); listMargin.add(params.bottomMargin); return listMargin; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { // 滑动未结束,内部使用scrollTo方法完成实际滑动 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } else { //滑动完成 isScrolling = false; if (listener != null) { listener.onChange(currentIndex,tabs[currentIndex]); } } super.computeScroll(); } /** * 改变选中TextView的颜色 * * @param currentIndex 滑动之前选中的那个 * @param nextIndex 滑动之后选中的那个 */ public final void scrollToNext(int currentIndex, int nextIndex) { TextView selectedText = (TextView) getChildAt(currentIndex); if (selectedText != null) { selectedText.setTextColor(normalTextColor); selectedText.setBackground(null); } selectedText = (TextView) getChildAt(nextIndex); if (selectedText != null) { changeSelectedUIState(selectedText); } } private void changeSelectedUIState(TextView view) { view.setTextColor(selectedTextColor); if (selectedTextBackgroundDrawable != null) { view.setBackground(selectedTextBackgroundDrawable); } if (selectedTextBackgroundColor != 0) { view.setBackgroundColor(selectedTextBackgroundColor); } if (selectedTextBackgroundResources != 0) { view.setBackgroundResource(selectedTextBackgroundResources); } } /** * 向右滑一个 */ public void moveToRight() { moveTo(getChildAt(currentIndex - 1)); } /** * 向左滑一个 */ public void moveToLeft() { moveTo(getChildAt(currentIndex + 1)); } /** * 滑到目标view * * @param view 目标view */ private void moveTo(View view) { for (int i = 0; i < getChildCount(); i++) { if (view == getChildAt(i)) { if (i == currentIndex) { //不移动 break; } else if (i < currentIndex) { //向右移 if (isScrolling) { return; } isScrolling = true; int dx = getChildAt(currentIndex).getLeft() - view.getLeft() + (getChildAt(currentIndex).getMeasuredWidth() - view.getMeasuredWidth()) / 2; //这里使用scroll会使滑动更平滑不卡顿,scroll会根据起点、终点及时间计算出每次滑动的距离,其内部有一个插值器 mScroller.startScroll(getScrollX(), 0, -dx, 0, mDuration); scrollToNext(currentIndex, i); setCurrentIndex(i); invalidate(); } else if (i > currentIndex) { //向左移 if (isScrolling) { return; } isScrolling = true; int dx = view.getLeft() - getChildAt(currentIndex).getLeft() + (view.getMeasuredWidth() - getChildAt(currentIndex).getMeasuredWidth()) / 2; mScroller.startScroll(getScrollX(), 0, dx, 0, mDuration); scrollToNext(currentIndex, i); setCurrentIndex(i); invalidate(); } } } } /** * 设置tabs * * @param tabs */ public void setTabs(Tab... tabs) { this.tabs = tabs; //暂时不通过layout布局添加textview if (getChildCount()>0){ removeAllViews(); } for (Tab tab : tabs) { TextView textView = new TextView(getContext()); textView.setText(tab.getText()); textView.setTextSize(14); textView.setTextColor(selectedTextColor); textView.setPadding(dp2px(getContext(),5), dp2px(getContext(),2), dp2px(getContext(),5),dp2px(getContext(),2)); LayoutParams layoutParams= new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT); layoutParams.rightMargin=dp2px(getContext(),2.5f); layoutParams.leftMargin=dp2px(getContext(),2.5f); textView.setLayoutParams(layoutParams); addView(textView); } } public int getCurrentIndex() { return currentIndex; } //设置默认选中第几个 public void setCurrentIndex(int currentIndex) { this.currentIndex = currentIndex; } //设置滑动时间 public void setDuration(int mDuration) { this.mDuration = mDuration; } public void setSelectedTextColor(int selectedTextColor) { this.selectedTextColor = selectedTextColor; } public void setNormalTextColor(int normalTextColor) { this.normalTextColor = normalTextColor; } public void setSelectedTextBackgroundDrawable(Drawable selectedTextBackgroundDrawable) { this.selectedTextBackgroundDrawable = selectedTextBackgroundDrawable; } public void setSelectedTextBackgroundColor(int selectedTextBackgroundColor) { this.selectedTextBackgroundColor = selectedTextBackgroundColor; } public void setSelectedTextBackgroundResources(int selectedTextBackgroundResources) { this.selectedTextBackgroundResources = selectedTextBackgroundResources; } public interface OnSelectedChangedListener { void onChange(int index, Tab tag); } private OnSelectedChangedListener listener; public void setOnSelectedChangedListener(OnSelectedChangedListener listener) { if (listener != null) { this.listener = listener; } } private int dp2px(Context context, float dpValue) { DisplayMetrics metrics = context.getResources().getDisplayMetrics(); return (int) (metrics.density * dpValue + 0.5F); } public interface Tab{ String getText(); } private float startX = 0f; @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { startX = event.getX(); } if (event.getAction() == MotionEvent.ACTION_UP) { float endX = event.getX(); //向左滑条件 if (endX - startX > 50 && currentIndex > 0) { moveToRight(); } if (startX - endX > 50 && currentIndex < getChildCount() - 1) { moveToLeft(); } } return true; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { startX = event.getX(); } if (event.getAction() == MotionEvent.ACTION_UP) { float endX = event.getX(); //向左滑条件 if (Math.abs(startX-endX)>50){ onTouchEvent(event); } } return super.onInterceptTouchEvent(event); } }

在Activity或fragment中使用

private var tabs = listOf("慢动作", "短视频", "录像", "拍照", "108M", "人像", "夜景", "萌拍", "全景", "专业") lateinit var imageAnalysis:ImageAnalysis override fun initView() { //实现了CameraIndicator.Tab的对象 val map = tabs.map { CameraIndicator.Tab { it } }?.toTypedArray() ?: arrayOf() //将tab集合设置给cameraIndicator,(binding.cameraIndicator即xml布局里的控件) binding.cameraIndicator.setTabs(*map) //默认选中 拍照 binding.cameraIndicator.currentIndex = 3 //点击某个tab的回调 binding.cameraIndicator.setSelectedTextBackgroundResources(R.drawable.selected_text_bg) binding.cameraIndicator.setOnSelectedChangedListener { index, tag -> Toast.makeText(this,tag.text,Toast.LENGTH_SHORT).show() } }



小米 相机 Android

需要 登录 后方可回复, 如果你还没有账号请 注册新账号