有趣的自定义View — 小米MIUI10相机·滑动功能指示器

本文详述了如何在Android应用中实现类似iOS相机的滑动切换功能,包括自定义ViewGroup、处理滑动事件、动画效果及UI更新等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

小米MIUI10相机功能滑动指示器效果如下:

  

可以看到2018年新出来的旗舰机,包括OPPO ViVo等,相机的交互都改为了左右滑动调整相机功能。

(其实都是仿iOS相机)如下图所示:

一、效果要求

1)短视频、视频、拍照、人像、正方形、全景、手动一个七个滑动指示器摆放到一起;

2)手指在屏幕上左右滑动时,滑动指示器也跟随手指左右滑动;

3)手指抬起时,滑动指示器停止滑动,被选中的那一个指示器位于屏幕中心位置;

4)支持慢速滑动和快速滑动;

二、实现难点及实现方法

1)自定义View-要求包裹住七个滑动指示器,ViewGroup没跑了;

2)每次滑动,七个指示器的位置是一起变化的,所以每次都要重新测量子view的位置和大小;

3)每次滑动,滑动距离的计算;

4)每次滑动之后,对应相机功能、UI、动画的执行;

三、上代码,具体实现

按照上述需求,一步步实现:

1)首先自定义ViewGroup,继承自系统ViewGroup;


/**
 * 自定义IndicatorScroller 继承自系统ViewGroup
 */
public class IndicatorScroller extends ViewGroup {
    // 利用Scroller类实现最终的滑动效果
    public Scroller mScroller;
    public static boolean mIsLayoutView = false;
    public IndicatorScroller(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
        mIsLayoutView = false;
    }
}

2)重写onLayout()和onMeasure()方法,放置子view;

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (mIsLayoutView) {
            return;
        }
        mIsLayoutView = true;
        int cCount = getChildCount();
        int childLeft = 0;
        int childRight = 0;
        int selectedMode = IndicatorUtils.getCurrentSelectedIndex();
        int widthOffset = 0;//居中显示
        /**
         * 遍历所有childView根据其宽和高,不考虑margin
         */
        for (int i = 0; i < cCount; i++) {
            View childView = getChildAt(i);
            if (i < selectedMode) {
                widthOffset += childView.getMeasuredWidth();
            }
        }

        for (int i = 0; i < cCount; i++) {
            View childView = getChildAt(i);
            if (i != 0) {
                View preView = getChildAt(i - 1);
                childLeft = preView.getRight();
                childRight = childLeft + childView.getMeasuredWidth();
            } else {                
                childLeft = (getWidth() - getChildAt(selectedMode).getMeasuredWidth()) / 2 - widthOffset;
                childRight = childLeft + childView.getMeasuredWidth();
            }
            childView.layout(childLeft, top, childRight, bottom);
        }

        TextView indexText = (TextView) getChildAt(selectedMode);
        indexText.setTextColor(0xff1996ff);
    }

①onLayout方法中,我们先自主选定一个初始默认选中的tab,其索引值用selectedMode表示;然后开始计算每个子textview的left、right、top、bottom值:如果index!=0,那么简单,只需计算前一个指示器的getRight值,然后加上自身的宽度即可得到该view的left值;如果index=0,那么就需要先根据selectedMode计算当前选中tab之前指示器的宽度之和,然后将ViewGroup的宽度减去selectedMode指示器的宽度,得到的值取半再减去之前计算的宽度之和,就可以得到index=0时的getLeft值。图示如下:

最后在调用View的onLayout(int l, int t, int r, int b)方法,将参数一一传入,得到每个子view的具体位置。

注:onLayout方法接收四个参数-l、t、r、b,分别是当前View从左、上、右、下相对于其父容器的距离。即值一一对应view的getLeft、getTop、getRight、getBottom方法

    @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的情况
        if (getChildCount() == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            View childOne = getChildAt(0);
            int childWidth = childOne.getMeasuredWidth();
            int childHeight = childOne.getMeasuredHeight();
            setMeasuredDimension(childWidth * getChildCount(), childHeight);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            View childOne = getChildAt(0);
            int childWidth = childOne.getMeasuredWidth();
            setMeasuredDimension(childWidth * getChildCount(), heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            int childHeight = getChildAt(0).getMeasuredHeight();
            setMeasuredDimension(widthSize, childHeight);
        }
        //如果自定义ViewGroup之初就已确认该ViewGroup宽高都是match_parent,那么直接设置即可
        setMeasuredDimension(widthSize, heightSize);
    }

②onMeasure方法中,自定义ViewGroup的标准onMeasure处理方法如上↑↑↑,自定义ViewGroup的一大难点就是要支持wrap_content属性,这里就不展开了,本例中已经确认IndicatorScroller作为父容器,宽高都是设为match_parent,所以直接将子textView的宽高丢入setMeasuredDimension()方法中,用以设置ViewGroup本身宽高即可。

3)重写computeScroll()实现过渡滑动;

   @Override
   public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            // 滑动未结束,内部使用scrollTo方法完成实际滑动
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        } else {
            // 滑动结束,执行对应的操作,建议利用接口进行监听
            Log.d("computeScroll", "滑动结束,执行对应操作");
        }
        super.computeScroll();
    }

如之前分析那样,本例使用Scroller类来实现滑动指示器的过渡性滑动。Scroller类本身是不能实现View的滑动的,它需要与view的computeScroll()方法配合才能实现弹性滑动效果。

重写computeScroll()方法,系统会在绘制View时在draw()方法中调用该方法。在这个方法中我们调用父类的scrollTo方法并通过Scroller来不断获取当前的滑动距离,每滑动一小段距离就再次调用invalidate()方法不断进行重绘,重绘就会调用computeScroll方法,这样我们通过不断移动一个小的距离并连贯起来就实现了平滑移动的效果。

注意这里调用了mScroller.computeScrollOffset()方法进行是否滑动结束的判断:computeScrollOffset()返回值为true,则表示滑动未结束,返回值为false,则滑动结束。

4)每次滑动之后,改变对应位置textView的颜色;

    public final void scrollToNext(int preIndex, int nextIndex) {
        TextView selectedText = (TextView) getChildAt(preIndex);
        if (selectedText != null) {
            selectedText.setTextColor(0xffffffff);
        }
        selectedText = (TextView) getChildAt(nextIndex);
        if (selectedText != null) {
            selectedText.setTextColor(0xff1996ff);
        }
    }

这里定义一个方法,接收两个TextView的索引值,如果是preIndex,则将颜色变为原色,如果是nextIndex,则将text的颜色进行变色处理,以指示现在选中的tab。

使用IndicatorScroller,我们创建一个xml文件,然后在IndicatorScroller内部放入三个TextView:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:background="@drawable/camera_mode_index_icon_point"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="3.0dip"
        android:layout_gravity="center"
        />

   <com.chin_style.view.IndicatorScroller
        android:id="@+id/camera_scroller"
        android:layout_width="wrap_content"
        android:layout_height="30dp">
        <TextView android:text="@string/camera_video" style="@style/cameraScrollerBar" />
        <TextView android:text="@string/camera_take_photo" style="@style/cameraScrollerBar" />
        <TextView android:text="@string/camera_square" style="@style/cameraScrollerBar" />
   </com.chin_style.view.IndicatorScroller>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="MaterialTheme" parent="android:Theme.Material.Light.NoActionBar.Fullscreen" />

    <style name="cameraScrollerBar">
        <item name="android:textSize">13.0sp</item>
        <item name="android:textColor">@android:color/black</item>
        <item name="android:gravity">center</item>
        <item name="android:paddingLeft">6.0dip</item>
        <item name="android:paddingRight">6.0dip</item>
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
    </style>
</resources>

5)再自定义一个View,用以引入IndicatorScroller,和处理对应的滑动事件和接口监听逻辑;

public class IndicatorView extends LinearLayout {
    private IndicatorScroller mIndicatorScroller;
    private Context mContext;

    public IndicatorView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        LayoutInflater.from(context).inflate(R.layout.indicator_scroller_layout, this, true);
    }

    public void init() {
        mIndicatorScroller = (IndicatorScroller) findViewById(R.id.camera_scroller);
    }

    private IndicatorScroller indicatorScroller;
    Handler leftHandler = new Handler();
    Runnable moveLeft_thread = new Runnable() {
        public void run() {
            mOnScrollerViewStausChange.OnScrollerViewStausChange(indicatorScroller.mLeftIndex);
        }
    };

    Handler rightHandler = new Handler();
    Runnable moveRight_thread = new Runnable() {
        public void run() {
            mOnScrollerViewStausChange.OnScrollerViewStausChange(indicatorScroller.mRightIndex);
        }
    };

    public void moveLeft(boolean is_last) {
        IndicatorUtils.sIsClickerIndicator = false;
        if (is_last) {
            IndicatorUtils.sIsFasterScroller = false;
        } else {
            IndicatorUtils.sIsFasterScroller = true;
        }
        indicatorScroller = mIndicatorScroller;
        indicatorScroller.mLeftIndex = IndicatorUtils.getCurrentSelectedIndex() - 1;
        indicatorScroller.mRightIndex = IndicatorUtils.getCurrentSelectedIndex();
        int k = Math.round((indicatorScroller.getChildAt(indicatorScroller.mLeftIndex).getWidth() + indicatorScroller.getChildAt(indicatorScroller.mRightIndex).getWidth()) / 2.0F);
        indicatorScroller.mScroller.startScroll(indicatorScroller.getScrollX(), 0, -k, 0, indicatorScroller.mDuration);
        indicatorScroller.scrollToNext(indicatorScroller.mRightIndex, indicatorScroller.mLeftIndex);
        IndicatorUtils.setSelectedIndex(IndicatorUtils.getCurrentSelectedIndex() - 1);
        indicatorScroller.invalidate();
        if (mOnScrollerViewStausChange != null) {
            if (is_last) {
                leftHandler.removeCallbacks(moveLeft_thread);
                leftHandler.post(moveLeft_thread);
            } else if (!is_last) {
                leftHandler.removeCallbacks(moveLeft_thread);
                leftHandler.postDelayed(moveLeft_thread, 950);
                mOnScrollerViewStausChange.OnScrollerViewUIChange(indicatorScroller.mLeftIndex);
            }
        }
    }

    public void moveRight(boolean is_last) {
        IndicatorUtils.sIsClickerIndicator = false;
        if (is_last) {
            IndicatorUtils.sIsFasterScroller = false;
        } else {
            IndicatorUtils.sIsFasterScroller = true;
        }
        indicatorScroller = mIndicatorScroller;
        indicatorScroller.mLeftIndex = IndicatorUtils.getCurrentSelectedIndex();
        indicatorScroller.mRightIndex = IndicatorUtils.getCurrentSelectedIndex() + 1;
        int k = Math.round((indicatorScroller.getChildAt(indicatorScroller.mLeftIndex).getWidth() + indicatorScroller.getChildAt(indicatorScroller.mRightIndex).getWidth()) / 2.0F);
        indicatorScroller.mScroller.startScroll(indicatorScroller.getScrollX(), 0, k, 0, indicatorScroller.mDuration);
        indicatorScroller.scrollToNext(indicatorScroller.mLeftIndex, indicatorScroller.mRightIndex);
        IndicatorUtils.setSelectedIndex(IndicatorUtils.getCurrentSelectedIndex() + 1);
        indicatorScroller.invalidate();
        if (mOnScrollerViewStausChange != null ) {
            if (is_last) {
                rightHandler.removeCallbacks(moveRight_thread);
                rightHandler.post(moveRight_thread);
            } else if (!is_last) {
                rightHandler.removeCallbacks(moveRight_thread);
                rightHandler.postDelayed(moveRight_thread, 950);
                mOnScrollerViewStausChange.OnScrollerViewUIChange(indicatorScroller.mRightIndex);
            }
        }
    }

    public IndicatorScroller getIndicatorScroller() {
        return mIndicatorScroller;
    }

    public interface OnScrollerViewStausChange {
        void OnScrollerViewStausChange(int position);
        void OnScrollerViewUIChange(int position);
    }

    public OnScrollerViewStausChange mOnScrollerViewStausChange;

    public void setOnScrollerViewStausChange(OnScrollerViewStausChange onScrollerViewStausChange) {
        mOnScrollerViewStausChange = onScrollerViewStausChange;
    }

}

IndicatorView处理的逻辑:

①引入之前加入IndicatorScroller的布局indicator_scroller_layout作为该viewGroup的UI;

②定义moveLeftmoveRight两个方法,其内部计算滑动值K,然后调用mScroller.startScroll()方法进行滑动;

③定义接口OnScrollerViewStausChange,发生滑动之后,在moveLeft和moveRight方法内部调用接口中的方法OnScrollerViewStausChange和OnScrollerViewUIChange,执行对应的操作;

④moveLeft和moveRight方法接收一个布尔值,用以判定是快速滑动还是慢速滑动,不同的滑动执行不同的逻辑;

6)Activity的XML布局中引入IndicatorView;

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">    
    ... ...

    <com.chin_style.view.IndicatorView
        android:id="@+id/bottomView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom">        
    </com.chin_style.view.IndicatorView>
</LinearLayout>

7)Activity中重写onTouchEvent方法,处理滑动逻辑;

public class MainActivity extends Activity {

    float x1=0;
    float x2=0;
    float y1=0;
    float y2=0;
    private IndicatorView mIndecatorView;

    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera);
        mIndecatorView=(BottomView)findViewById(R.id.bottomView);
        mIndecatorView.init();
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event){

        if(event.getAction()==MotionEvent.ACTION_DOWN){
            x1=event.getX();
            y1=event.getY();
        }
        if(event.getAction()==MotionEvent.ACTION_UP){
            x2=event.getX();
            y2=event.getY();

            if(x1-x2>50){
                if(Util.getCurrentSelectedIndex()<Util.MAX_INDEX) {
                    mIndecatorView.moveRight();
                }
            }else if(x2-x1>50){
                if(Util.getCurrentSelectedIndex()>Util.MIN_INDEX) {
                    mIndecatorView.moveLeft();
                }
            }
        }
        return super.onTouchEvent(event);
    }

}

最后附上,使用到的 IndicatorUtils

public class IndicatorUtils {
    public final static int MIN_INDEX = 0;
    public final static int MAX_INDEX = 5;
    public static int sSelectposition = 2;

    public static long sChangeFuntionTime = 0;

    public static boolean sIsFasterScroller = false;

    public static boolean sIsReStartCameraEnd = false;

    public static boolean sIsClickerIndicator = false;

    public static int getCurrentSelectedIndex() {
        return sSelectposition;
    }

    public static void setSelectedIndex(int index) {
        sSelectposition = index;
    }
}

效果如下:有点丑陋,为了测试嘛,可以理解!

 

参考文章:《Android上类似于iOS相机滑动切换的View》

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值