Android开发之手势识别

Android手势识别:使用GestureOverlayView进行手势识别编程 在上述代码中,我们在手势结束时调用getGesture()方法获取用户绘制的手势对象,并使用getGestureStrokeType()方法获取手势的预测结果。手势识别是一种在Android应用中常见的交互方式,它可以让用户通过手指在屏幕上绘制特定的手势来触发相应的操作。设置手势识别的监听器:除了使用addOnGestureListener方法添加手势监听器外,还可以使用setOnGesturePerformedListener方法来设置手势识别的监听器,它可以在用户完成手势绘制后立即触发相应的操作。 阅读详情

在播放器中,涉及到手势识别。所以,今天我们来说一下Android的手势识别。

我们首先需要站在巨人的肩膀上。引用一些别人的案例和说明。

第一篇:

http://www.2cto.com/kf/201110/109480.html


对于触摸屏,其原生的消息无非按下、抬起、移动这几种,我们只需要简单重载onTouch或者设置触摸侦听器setOnTouchListener即可进行处理。不过,为了提高我们的APP的用户体验,有时候我们需要识别用户的手势,Android给我们提供的手势识别工具GestureDetector就可以帮上大忙了。


基础



GestureDetector的工作原理是,当我们接收到用户触摸消息时,将这个消息交给GestureDetector去加工,我们通过设置侦听器获得GestureDetector处理后的手势。


GestureDetector提供了两个侦听器接口,OnGestureListener处理单击类消息,OnDoubleTapListener处理双击类消息。


OnGestureListener的接口有这几个:


// 单击,触摸屏按下时立刻触发 


abstract boolean onDown(MotionEvent e); 


// 抬起,手指离开触摸屏时触发(长按、滚动、滑动时,不会触发这个手势) 


abstract boolean onSingleTapUp(MotionEvent e); 


// 短按,触摸屏按下后片刻后抬起,会触发这个手势,如果迅速抬起则不会 


abstract void onShowPress(MotionEvent e); 


// 长按,触摸屏按下后既不抬起也不移动,过一段时间后触发 


abstract void onLongPress(MotionEvent e); 


// 滚动,触摸屏按下后移动 


abstract boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY); 


// 滑动,触摸屏按下后快速移动并抬起,会先触发滚动手势,跟着触发一个滑动手势 


abstract boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); 


OnDoubleTapListener的接口有这几个:


// 双击,手指在触摸屏上迅速点击第二下时触发 


abstract boolean onDoubleTap(MotionEvent e); 


// 双击的按下跟抬起各触发一次 


abstract boolean onDoubleTapEvent(MotionEvent e); 


// 单击确认,即很快的按下并抬起,但并不连续点击第二下 


abstract boolean onSingleTapConfirmed(MotionEvent e); 


有时候我们并不需要处理上面所有手势,方便起见,Android提供了另外一个类SimpleOnGestureListener实现了如上接口,我们只需要继承SimpleOnGestureListener然后重载感兴趣的手势即可。

简单应用

import android.content.Context; 

import android.view.MotionEvent; 

import android.view.GestureDetector.SimpleOnGestureListener; 

import android.widget.Toast; 

 

public class MyGestureListener extends SimpleOnGestureListener { 

 

    private Context mContext; 

     

    MyGestureListener(Context context) { 

        mContext = context; 

    } 

     

    @Override 

    public boolean onDown(MotionEvent e) { 

        Toast.makeText(mContext, "DOWN " + e.getAction(), Toast.LENGTH_SHORT).show(); 

        return false; 

    } 

 

    @Override 

    public void onShowPress(MotionEvent e) { 

        Toast.makeText(mContext, "SHOW " + e.getAction(), Toast.LENGTH_SHORT).show();            

    } 

 

    @Override 

    public boolean onSingleTapUp(MotionEvent e) { 

        Toast.makeText(mContext, "SINGLE UP " + e.getAction(), Toast.LENGTH_SHORT).show(); 

        return false; 

    } 

 

    @Override 

    public boolean onScroll(MotionEvent e1, MotionEvent e2, 

            float distanceX, float distanceY) { 

        Toast.makeText(mContext, "SCROLL " + e2.getAction(), Toast.LENGTH_SHORT).show(); 

        return false; 

    } 

 

    @Override 

    public void onLongPress(MotionEvent e) { 

        Toast.makeText(mContext, "LONG " + e.getAction(), Toast.LENGTH_SHORT).show(); 

    } 

 

    @Override 

    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, 

            float velocityY) { 

        Toast.makeText(mContext, "FLING " + e2.getAction(), Toast.LENGTH_SHORT).show(); 

        return false; 

    } 

 

    @Override 

    public boolean onDoubleTap(MotionEvent e) { 

        Toast.makeText(mContext, "DOUBLE " + e.getAction(), Toast.LENGTH_SHORT).show(); 

        return false; 

    } 

 

    @Override 

    public boolean onDoubleTapEvent(MotionEvent e) { 

        Toast.makeText(mContext, "DOUBLE EVENT " + e.getAction(), Toast.LENGTH_SHORT).show(); 

        return false; 

    } 

 

    @Override 

    public boolean onSingleTapConfirmed(MotionEvent e) { 

        Toast.makeText(mContext, "SINGLE CONF " + e.getAction(), Toast.LENGTH_SHORT).show(); 

        return false; 

    } 

} 

 

 
我们可以在Activity里设置手势识别:
import android.app.Activity; 

import android.os.Bundle; 

import android.view.GestureDetector; 

import android.view.MotionEvent; 

 

public class GestureTestActivity extends Activity { 

    private GestureDetector mGestureDetector; 

 

    @Override 

    public void onCreate(Bundle savedInstanceState) { 

        super.onCreate(savedInstanceState); 

        setContentView(R.layout.main); 

 

        mGestureDetector = new GestureDetector(this, new MyGestureListener(this)); 

    } 

 

    @Override 

    public boolean onTouchEvent(MotionEvent event) { 

        return mGestureDetector.onTouchEvent(event); 

    } 

} 

自定View中使用手势识别

import android.content.Context; 

import android.util.AttributeSet; 

import android.view.GestureDetector; 

import android.view.MotionEvent; 

import android.view.View; 

 

public class MyView extends View { 

 

    private GestureDetector mGestureDetector; 

 

    public MyView(Context context, AttributeSet attrs) { 

        super(context, attrs); 

 

        mGestureDetector = new GestureDetector(context, new MyGestureListener(context)); 

 

        setLongClickable(true); 

 

        this.setOnTouchListener(new OnTouchListener() { 

 

            public boolean onTouch(View v, MotionEvent event) { 

                return mGestureDetector.onTouchEvent(event); 

            } 

 

        }); 

    } 

} 

 

需要注意的问题:

对于自定义View,使用手势识别有两处陷阱可能会浪费你的不少时间。

1:View必须设置longClickable为true,否则手势识别无法正确工作,只会返回Down, Show, Long三种手势

2:必须在View的onTouchListener中调用手势识别,而不能像Activity一样重载onTouchEvent,否则同样手势识别无法正确工作


测试结果


下面是各种操作返回的手势序列,数值0表示触摸屏按下,1表示抬起

单击:down 0, single up 1, single conf 0 


短按:down 0, show 0, single up 1 


长按:down 0, show 0, long 0 


双击:down 0, single up 1, double 0, double event 0, down 0, double event 1 


滚动:down 0, (show 0), scrool 2... 


滑动:down 0, (show 0), scrool 2..., fling 1   


手势滑动在播放器中的应用

我们可以通过手势的滑动来调节音量,来控制进度。

package com.kankan.anime.player;

import android.app.Activity;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import com.kankan.anime.R;
import com.kankan.anime.player.GestureDetector.SimpleOnGestureListener;
import com.kankan.anime.player.local.LocalPlayerActivity;
import com.kankan.anime.util.NetworkHelper;
import com.kankan.anime.util.UIHelper;
import com.kankan.anime.widget.MediaController;
import com.kankan.anime.widget.MediaController.MediaPlayerControl;
import com.kankan.anime.widget.VideoGestureSeekWidget;
import com.kankan.anime.widget.VoiceLightWidget;
import com.kankan.logging.Logger;

public class GestureDelegator {
    private static final Logger LOG = Logger.getLogger(GestureDelegator.class);

    private static final double RADIUS_SLOP = Math.PI * 5 / 24;

    private static final int GESTURE_NONE = 0;
    private static final int GESTURE_VOICE = GESTURE_NONE + 1;
    private static final int GESTURE_LIGHT = GESTURE_VOICE + 1;
    private static final int GESTURE_PROGRESS = GESTURE_LIGHT + 1;
    private static final int MAX_SEEK_TIME = (int) (1.5 * 60);// 屏幕滑动快进,滑动一屏幕是180s

    private VoiceLightWidget mVoiceLightWidget;
    private VideoGestureSeekWidget mSeekWidget;
    private int mCurrentGesture;
    private final MediaController mMediaController;
    private final GestureDetector mGestureDetector;
    private final MediaController.MediaPlayerControl mPlayerController;
    private final Activity mContext;
    private Fragment mFragment;

    private int mDragPos;
    private int mCurrentDeltaScroll;
    private int mScrolledPixPerVideoSecend;
    private int mDeltaAll = 0;
    private boolean mNeedResume;

    public GestureDelegator(Fragment fragment, MediaController mediaController,
            MediaPlayerControl mediaPlayerControl) {
        mMediaController = mediaController;
        mFragment = fragment;
        mContext = fragment.getActivity();
        mPlayerController = mediaPlayerControl;
        mScrolledPixPerVideoSecend = (int) (UIHelper.getScreenWidth(mContext) * 0.7) / MAX_SEEK_TIME;

        mGestureDetector = new GestureDetector(mContext, mGestureListener);

        attachVoiceControllerToActivity();
    }

    private void clearDragPos() {
        mDragPos = 0;
        mDeltaAll = 0;
    }

    public boolean onTouchEvent(MotionEvent ev) {
        if (mMediaController.isShowing() && (mMediaController.isActionInPannel(ev) && !mMediaController.isLocked())) {
            mMediaController.show();

            return true;
        }

        final int action = ev.getAction();
        if (action == MotionEvent.ACTION_UP
                || action == MotionEvent.ACTION_CANCEL) {
            if (mCurrentGesture == GESTURE_PROGRESS) {
                if (mNeedResume) {
                    mPlayerController.start();
                    mNeedResume = !mNeedResume;
                }

                mPlayerController.seekTo(mDragPos);

                clearDragPos();
            }

            if (mCurrentGesture == GESTURE_NONE) {
                if (mMediaController.isLocked()) {
                    if (!mMediaController.isShowing()) {
                        mMediaController.show();
                    } else {
                        mMediaController.hide();
                    }
                } else {
                    if (!mMediaController.isShowing()) {
                        mMediaController.show();
                        mMediaController.showSystemUI();
                    } else {
                        mMediaController.hide();
                        mMediaController.hideSystemUI();
                    }
                }
            }

            if (mMediaController.isShowing() && mCurrentGesture != GESTURE_NONE) {
                mMediaController.fadeOut(1000);
            }
            mCurrentGesture = GESTURE_NONE;
        }

        if (action == MotionEvent.ACTION_MOVE) {
            if (mMediaController.isShowing()) {
                mMediaController.show();
            }
        }
        if (!mMediaController.isLocked()) {
            mGestureDetector.onTouchEvent(ev);
        }

        return true;
    }

    private void attachVoiceControllerToActivity() {
        ViewGroup outFrame = (ViewGroup) mFragment.getView();
        View layer = LayoutInflater.from(mContext).inflate(R.layout.gesture_widget_layer, null);
        mVoiceLightWidget = (VoiceLightWidget) layer.findViewById(R.id.voice_controller);
        mSeekWidget = (VideoGestureSeekWidget) layer.findViewById(R.id.video_seek_controller);
        if (outFrame != null) {
            outFrame.addView(layer);
        }
    }

    private SimpleOnGestureListener mGestureListener = new SimpleOnGestureListener() {

        public boolean onDoubleTap(MotionEvent e) {
            if (mPlayerController.isPlaying()) {
                mPlayerController.pause();
            } else {
                if (mContext instanceof LocalPlayerActivity) {
                    mPlayerController.start();
                } else {
                    NetworkHelper.getInstance().accessNetwork(mContext, new Runnable() {

                        @Override
                        public void run() {
                            mPlayerController.start();
                        }
                    });
                }
            }
            return true;
        };

        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                float distanceX, float distanceY) {
            if (e1 == null || e2 == null) {
                return false;
            }
            float oldX = e1.getX();
            final double distance = Math.sqrt(Math.pow(distanceX, 2)
                    + Math.pow(distanceY, 2));
            int windowWidth = UIHelper.getScreenWidth(mContext);
            final double radius = distanceY / distance;

            if (Math.abs(radius) > RADIUS_SLOP) {
                if (mCurrentGesture != GESTURE_PROGRESS
                        && !mSeekWidget.isVisiable()) {
                    if (oldX > windowWidth / 2) {// TODO右半屏幕处理声音的逻辑
                        mCurrentGesture = GESTURE_VOICE;
                        onVoiceChange(distanceY, distance);
                    } else {// TODO左半屏幕处理亮度的逻辑
                        mCurrentGesture = GESTURE_LIGHT;
                        onLightChange(distanceY, distance);
                    }
                }
            } else {// TODO 处理视频进度
                if (mCurrentGesture != GESTURE_VOICE
                        && mCurrentGesture != GESTURE_LIGHT
                        && !mVoiceLightWidget.isVisible()) {
                    onVideoTouchSeek(distanceX, distance);
                }
            }

            return super.onScroll(e1, e2, distanceX, distanceY);
        }
    };

    private void onVoiceChange(float delta, double distance) {
        mSeekWidget.setVisibility(View.GONE);
        mVoiceLightWidget.onVoiceChange(delta, (int) distance);
    }

    private void onLightChange(float delta, double distance) {
        mSeekWidget.setVisibility(View.GONE);
        mVoiceLightWidget.onLightChange(delta, (int) distance,
                mContext.getWindow());
    }

    private void onVideoTouchSeek(float distanceX, double distane) {
        mVoiceLightWidget.setVisibility(View.GONE);

        if (mDragPos == 0 && mCurrentGesture != GESTURE_PROGRESS) {
            mDragPos = mPlayerController.getCurrentPosition();
        }

        if (mPlayerController.isPlaying()) {
            mPlayerController.pause();
            mNeedResume = true;
        }

        mCurrentGesture = GESTURE_PROGRESS;

        mCurrentDeltaScroll += distanceX;
        
        if (Math.abs(mCurrentDeltaScroll) >= mScrolledPixPerVideoSecend) {
            int deltaTime = mCurrentDeltaScroll / mScrolledPixPerVideoSecend;
            mDeltaAll += deltaTime;
            mDragPos = mDragPos - deltaTime * 1000;
            if (mDragPos > mPlayerController.getDuration()) {
                mDragPos = mPlayerController.getDuration();
            }
            if (mDragPos < 0) {
                mDragPos = 0;
                mDeltaAll = 0;
            }

            mCurrentDeltaScroll = 0;
        }

        mSeekWidget.onSeek(mDragPos, mPlayerController.getDuration(), mDeltaAll);
    }
}
最后我们附上

GestureDetector.java文件

package com.kankan.anime.player;

/*
 * Copyright (C) 2008 The Android Open Source Project
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;

import com.kankan.logging.Logger;

/**
 * Detects various gestures and events using the supplied {@link MotionEvent}s. The {@link OnGestureListener} callback
 * will notify users when a particular motion event has occurred. This class should only be used with
 * {@link MotionEvent}s reported via touch (don't use for trackball events).
 * 
 * To use this class:
 * <ul>
 * <li>Create an instance of the {@code GestureDetector} for your {@link View}
 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call {@link #onTouchEvent(MotionEvent)}. The
 * methods defined in your callback will be executed when the events occur.
 * </ul>
 */
public class GestureDetector {
    @SuppressWarnings("unused")
    private static final Logger LOG = Logger.getLogger(GestureDetector.class);

    /**
     * The listener that is used to notify when gestures occur. If you want to listen for all the different gestures
     * then implement this interface. If you only want to listen for a subset it might be easier to extend
     * {@link SimpleOnGestureListener}.
     */
    public interface OnGestureListener {

        /**
         * Notified when a tap occurs with the down {@link MotionEvent} that triggered it. This will be triggered
         * immediately for every down event. All other events should be preceded by this.
         * 
         * @param e
         *            The down motion event.
         */
        boolean onDown(MotionEvent e);

        /**
         * The user has performed a down {@link MotionEvent} and not performed a move or up yet. This event is commonly
         * used to provide visual feedback to the user to let them know that their action has been recognized i.e.
         * highlight an element.
         * 
         * @param e
         *            The down motion event
         */
        void onShowPress(MotionEvent e);

        /**
         * Notified when a tap occurs with the up {@link MotionEvent} that triggered it.
         * 
         * @param e
         *            The up motion event that completed the first tap
         * @return true if the event is consumed, else false
         */
        boolean onSingleTapUp(MotionEvent e);

        /**
         * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the current move
         * {@link MotionEvent}. The distance in x and y is also supplied for convenience.
         * 
         * @param e1
         *            The first down motion event that started the scrolling.
         * @param e2
         *            The move motion event that triggered the current onScroll.
         * @param distanceX
         *            The distance along the X axis that has been scrolled since the last call to onScroll. This is NOT
         *            the distance between {@code e1} and {@code e2}.
         * @param distanceY
         *            The distance along the Y axis that has been scrolled since the last call to onScroll. This is NOT
         *            the distance between {@code e1} and {@code e2}.
         * @return true if the event is consumed, else false
         */
        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);

        /**
         * Notified when a long press occurs with the initial on down {@link MotionEvent} that trigged it.
         * 
         * @param e
         *            The initial on down motion event that started the longpress.
         */
        void onLongPress(MotionEvent e);

        /**
         * Notified of a fling event when it occurs with the initial on down {@link MotionEvent} and the matching up
         * {@link MotionEvent}. The calculated velocity is supplied along the x and y axis in pixels per second.
         * 
         * @param e1
         *            The first down motion event that started the fling.
         * @param e2
         *            The move motion event that triggered the current onFling.
         * @param velocityX
         *            The velocity of this fling measured in pixels per second along the x axis.
         * @param velocityY
         *            The velocity of this fling measured in pixels per second along the y axis.
         * @return true if the event is consumed, else false
         */
        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
    }

    /**
     * The listener that is used to notify when a double-tap or a confirmed single-tap occur.
     */
    public interface OnDoubleTapListener {
        /**
         * Notified when a single-tap occurs.
         * <p>
         * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this will only be called after the detector is
         * confident that the user's first tap is not followed by a second tap leading to a double-tap gesture.
         * 
         * @param e
         *            The down motion event of the single-tap.
         * @return true if the event is consumed, else false
         */
        boolean onSingleTapConfirmed(MotionEvent e);

        /**
         * Notified when a double-tap occurs.
         * 
         * @param e
         *            The down motion event of the first tap of the double-tap.
         * @return true if the event is consumed, else false
         */
        boolean onDoubleTap(MotionEvent e);

        /**
         * Notified when an event within a double-tap gesture occurs, including the down, move, and up events.
         * 
         * @param e
         *            The motion event that occurred during the double-tap gesture.
         * @return true if the event is consumed, else false
         */
        boolean onDoubleTapEvent(MotionEvent e);
    }

    /**
     * A convenience class to extend when you only want to listen for a subset of all the gestures. This implements all
     * methods in the {@link OnGestureListener} and {@link OnDoubleTapListener} but does nothing and return
     * {@code false} for all applicable methods.
     */
    public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener {
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }

        public void onLongPress(MotionEvent e) {
        }

        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                float distanceX, float distanceY) {
            return false;
        }

        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                float velocityY) {
            return false;
        }

        public void onShowPress(MotionEvent e) {
        }

        public boolean onDown(MotionEvent e) {
            return false;
        }

        public boolean onDoubleTap(MotionEvent e) {
            return false;
        }

        public boolean onDoubleTapEvent(MotionEvent e) {
            return false;
        }

        public boolean onSingleTapConfirmed(MotionEvent e) {
            return false;
        }
    }

    private int mTouchSlopSquare;
    private int mDoubleTapTouchSlopSquare;
    private int mDoubleTapSlopSquare;
    private int mMinimumFlingVelocity;
    private int mMaximumFlingVelocity;

    /**
     * 解决huawei meit手动隐藏navigationBar导致 doubleTab失效问题
     */
    private float mDoubleTapSlopSquareFactor = 1.3f;

    private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
    private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();

    // constants for Message.what used by GestureHandler below
    private static final int SHOW_PRESS = 1;
    private static final int LONG_PRESS = 2;
    private static final int TAP = 3;

    private final Handler mHandler;
    private final OnGestureListener mListener;
    private OnDoubleTapListener mDoubleTapListener;

    private boolean mStillDown;
    private boolean mInLongPress;
    private boolean mAlwaysInTapRegion;
    private boolean mAlwaysInBiggerTapRegion;

    private MotionEvent mCurrentDownEvent;
    private MotionEvent mPreviousUpEvent;

    /**
     * True when the user is still touching for the second tap (down, move, and up events). Can only be true if there is
     * a double tap listener attached.
     */
    private boolean mIsDoubleTapping;

    private float mLastFocusX;
    private float mLastFocusY;
    private float mDownFocusX;
    private float mDownFocusY;

    private boolean mIsLongpressEnabled;

    /**
     * Determines speed during touch scrolling
     */
    private VelocityTracker mVelocityTracker;

    private class GestureHandler extends Handler {
        GestureHandler() {
            super();
        }

        GestureHandler(Handler handler) {
            super(handler.getLooper());
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case SHOW_PRESS:
                mListener.onShowPress(mCurrentDownEvent);
                break;

            case LONG_PRESS:
                dispatchLongPress();
                break;

            case TAP:
                // If the user's finger is still down, do not count it as a tap
                if (mDoubleTapListener != null && !mStillDown) {
                    mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                }
                break;

            default:
                throw new RuntimeException("Unknown message " + msg); // never
            }
        }
    }

    /**
     * Creates a GestureDetector with the supplied listener. This variant of the constructor should be used from a
     * non-UI thread (as it allows specifying the Handler).
     * 
     * @param listener
     *            the listener invoked for all the callbacks, this must not be null.
     * @param handler
     *            the handler to use
     * 
     * @throws NullPointerException
     *             if either {@code listener} or {@code handler} is null.
     * 
     * @deprecated Use
     *             {@link #GestureDetector(android.content.Context, android.view.GestureDetector.OnGestureListener, android.os.Handler)}
     *             instead.
     */
    @Deprecated
    public GestureDetector(OnGestureListener listener, Handler handler) {
        this(null, listener, handler);
    }

    /**
     * Creates a GestureDetector with the supplied listener. You may only use this constructor from a UI thread (this is
     * the usual situation).
     * 
     * @see android.os.Handler#Handler()
     * 
     * @param listener
     *            the listener invoked for all the callbacks, this must not be null.
     * 
     * @throws NullPointerException
     *             if {@code listener} is null.
     * 
     * @deprecated Use {@link #GestureDetector(android.content.Context, android.view.GestureDetector.OnGestureListener)}
     *             instead.
     */
    @Deprecated
    public GestureDetector(OnGestureListener listener) {
        this(null, listener, null);
    }

    /**
     * Creates a GestureDetector with the supplied listener. You may only use this constructor from a UI thread (this is
     * the usual situation).
     * 
     * @see android.os.Handler#Handler()
     * 
     * @param context
     *            the application's context
     * @param listener
     *            the listener invoked for all the callbacks, this must not be null.
     * 
     * @throws NullPointerException
     *             if {@code listener} is null.
     */
    public GestureDetector(Context context, OnGestureListener listener) {
        this(context, listener, null);
    }

    /**
     * Creates a GestureDetector with the supplied listener. You may only use this constructor from a UI thread (this is
     * the usual situation).
     * 
     * @see android.os.Handler#Handler()
     * 
     * @param context
     *            the application's context
     * @param listener
     *            the listener invoked for all the callbacks, this must not be null.
     * @param handler
     *            the handler to use
     * 
     * @throws NullPointerException
     *             if {@code listener} is null.
     */
    public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
        if (handler != null) {
            mHandler = new GestureHandler(handler);
        } else {
            mHandler = new GestureHandler();
        }
        mListener = listener;
        if (listener instanceof OnDoubleTapListener) {
            setOnDoubleTapListener((OnDoubleTapListener) listener);
        }
        init(context);
    }

    /**
     * Creates a GestureDetector with the supplied listener. You may only use this constructor from a UI thread (this is
     * the usual situation).
     * 
     * @see android.os.Handler#Handler()
     * 
     * @param context
     *            the application's context
     * @param listener
     *            the listener invoked for all the callbacks, this must not be null.
     * @param handler
     *            the handler to use
     * 
     * @throws NullPointerException
     *             if {@code listener} is null.
     */
    public GestureDetector(Context context, OnGestureListener listener, Handler handler,
            boolean unused) {
        this(context, listener, handler);
    }

    private void init(Context context) {
        if (mListener == null) {
            throw new NullPointerException("OnGestureListener must not be null");
        }
        mIsLongpressEnabled = true;

        // Fallback to support pre-donuts releases
        int touchSlop, doubleTapSlop, doubleTapTouchSlop;
        final ViewConfiguration configuration = ViewConfiguration.get(context);
        touchSlop = configuration.getScaledTouchSlop();
        // doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
        doubleTapTouchSlop = configuration.getScaledTouchSlop();
        doubleTapSlop = configuration.getScaledDoubleTapSlop();
        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
        mTouchSlopSquare = touchSlop * touchSlop;
        mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
        // mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
        mDoubleTapSlopSquare = (int) (doubleTapSlop * doubleTapSlop * mDoubleTapSlopSquareFactor);
    }

    /**
     * Sets the listener which will be called for double-tap and related gestures.
     * 
     * @param onDoubleTapListener
     *            the listener invoked for all the callbacks, or null to stop listening for double-tap gestures.
     */
    public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
        mDoubleTapListener = onDoubleTapListener;
    }

    /**
     * Set whether longpress is enabled, if this is enabled when a user presses and holds down you get a longpress event
     * and nothing further. If it's disabled the user can press and hold down and then later moved their finger and you
     * will get scroll events. By default longpress is enabled.
     * 
     * @param isLongpressEnabled
     *            whether longpress should be enabled.
     */
    public void setIsLongpressEnabled(boolean isLongpressEnabled) {
        mIsLongpressEnabled = isLongpressEnabled;
    }

    /**
     * @return true if longpress is enabled, else false.
     */
    public boolean isLongpressEnabled() {
        return mIsLongpressEnabled;
    }

    /**
     * Analyzes the given motion event and if applicable triggers the appropriate callbacks on the
     * {@link OnGestureListener} supplied.
     * 
     * @param ev
     *            The current motion event.
     * @return true if the {@link OnGestureListener} consumed the event, else false.
     */
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);

        final boolean pointerUp =
                (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
        final int skipIndex = pointerUp ? ev.getActionIndex() : -1;

        // Determine focal point
        float sumX = 0, sumY = 0;
        final int count = ev.getPointerCount();
        for (int i = 0; i < count; i++) {
            if (skipIndex == i)
                continue;
            sumX += ev.getX(i);
            sumY += ev.getY(i);
        }
        final int div = pointerUp ? count - 1 : count;
        final float focusX = sumX / div;
        final float focusY = sumY / div;

        boolean handled = false;

        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_POINTER_DOWN:
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            // Cancel long press and taps
            cancelTaps();
            break;

        case MotionEvent.ACTION_POINTER_UP:
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;

            // Check the dot product of current velocities.
            // If the pointer that left was opposing another velocity vector, clear.
            mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
            final int upIndex = ev.getActionIndex();
            final int id1 = ev.getPointerId(upIndex);
            final float x1 = mVelocityTracker.getXVelocity(id1);
            final float y1 = mVelocityTracker.getYVelocity(id1);
            for (int i = 0; i < count; i++) {
                if (i == upIndex)
                    continue;

                final int id2 = ev.getPointerId(i);
                final float x = x1 * mVelocityTracker.getXVelocity(id2);
                final float y = y1 * mVelocityTracker.getYVelocity(id2);

                final float dot = x + y;
                if (dot < 0) {
                    mVelocityTracker.clear();
                    break;
                }
            }
            break;

        case MotionEvent.ACTION_DOWN:
            if (mDoubleTapListener != null) {
                boolean hadTapMessage = mHandler.hasMessages(TAP);
                if (hadTapMessage)
                    mHandler.removeMessages(TAP);
                if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
                        isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
                    // This is a second tap
                    mIsDoubleTapping = true;
                    // Give a callback with the first tap of the double-tap
                    handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
                    // Give a callback with down event of the double-tap
                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                } else {
                    // This is a first tap
                    mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
                }
            }

            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            if (mCurrentDownEvent != null) {
                mCurrentDownEvent.recycle();
            }
            mCurrentDownEvent = MotionEvent.obtain(ev);
            mAlwaysInTapRegion = true;
            mAlwaysInBiggerTapRegion = true;
            mStillDown = true;
            mInLongPress = false;

            if (mIsLongpressEnabled) {
                mHandler.removeMessages(LONG_PRESS);
                mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
                        + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
            }
            mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
            handled |= mListener.onDown(ev);
            break;

        case MotionEvent.ACTION_MOVE:
            if (mInLongPress) {
                break;
            }
            final float scrollX = mLastFocusX - focusX;
            final float scrollY = mLastFocusY - focusY;

            if (mIsDoubleTapping) {
                // Give the move events of the double-tap
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else if (mAlwaysInTapRegion) {
                final int deltaX = (int) (focusX - mDownFocusX);
                final int deltaY = (int) (focusY - mDownFocusY);
                int distance = (deltaX * deltaX) + (deltaY * deltaY);
                if (distance > mTouchSlopSquare) {
                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                    mLastFocusX = focusX;
                    mLastFocusY = focusY;
                    mAlwaysInTapRegion = false;
                    mHandler.removeMessages(TAP);
                    mHandler.removeMessages(SHOW_PRESS);
                    mHandler.removeMessages(LONG_PRESS);
                }
                if (distance > mDoubleTapTouchSlopSquare) {
                    mAlwaysInBiggerTapRegion = false;
                }
            } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                mLastFocusX = focusX;
                mLastFocusY = focusY;
            }

            break;

        case MotionEvent.ACTION_UP:
            mStillDown = false;
            MotionEvent currentUpEvent = MotionEvent.obtain(ev);
            if (mIsDoubleTapping) {
                // Finally, give the up event of the double-tap
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else if (mInLongPress) {
                mHandler.removeMessages(TAP);
                mInLongPress = false;
            } else if (mAlwaysInTapRegion) {
                handled = mListener.onSingleTapUp(ev);
            } else {

                // A fling must travel the minimum tap distance
                final VelocityTracker velocityTracker = mVelocityTracker;
                final int pointerId = ev.getPointerId(0);
                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                final float velocityY = velocityTracker.getYVelocity(pointerId);
                final float velocityX = velocityTracker.getXVelocity(pointerId);

                if ((Math.abs(velocityY) > mMinimumFlingVelocity)
                        || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
                    handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
                }
            }
            if (mPreviousUpEvent != null) {
                mPreviousUpEvent.recycle();
            }
            // Hold the event we obtained above - listeners may have changed the original.
            mPreviousUpEvent = currentUpEvent;
            if (mVelocityTracker != null) {
                // This may have been cleared when we called out to the
                // application above.
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            mIsDoubleTapping = false;
            mHandler.removeMessages(SHOW_PRESS);
            mHandler.removeMessages(LONG_PRESS);
            break;

        case MotionEvent.ACTION_CANCEL:
            cancel();
            break;
        }

        return handled;
    }

    private void cancel() {
        mHandler.removeMessages(SHOW_PRESS);
        mHandler.removeMessages(LONG_PRESS);
        mHandler.removeMessages(TAP);
        mVelocityTracker.recycle();
        mVelocityTracker = null;
        mIsDoubleTapping = false;
        mStillDown = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        if (mInLongPress) {
            mInLongPress = false;
        }
    }

    private void cancelTaps() {
        mHandler.removeMessages(SHOW_PRESS);
        mHandler.removeMessages(LONG_PRESS);
        mHandler.removeMessages(TAP);
        mIsDoubleTapping = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        if (mInLongPress) {
            mInLongPress = false;
        }
    }

    private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
            MotionEvent secondDown) {
        if (!mAlwaysInBiggerTapRegion) {
            return false;
        }

        if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
            return false;
        }

        int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
        int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
        return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
    }

    private void dispatchLongPress() {
        mHandler.removeMessages(TAP);
        mInLongPress = true;
        mListener.onLongPress(mCurrentDownEvent);
    }
}


好,Android的手势识别就到这里。谢谢。



15、Android 手势识别开发全解析 本文详细解析了Android平台下的手势识别开发全过程,涵盖常见手势检测与自定义手势识别的实现方法。通过创建GestureDetectorCompat实例和使用GestureOverlayView结合GestureLibrary,开发者可轻松实现滑动、双击、圆形、X形等手势的识别与响应。文章还介绍了如何利用SDK内置的GestureBuilder应用创建手势文件、提取并集成到项目中,并通过完整的代码示例和流程图指导读者完成从环境搭建到应用测试的每一步,帮助提升Android应用的交互体验。 阅读详情

相关推荐

Android应用开发:手势识别编程指南

在Android应用开发中,手势识别是一个常见而有趣的功能,可以为用户提供更直观、交互性更强的体验。本文将介绍如何在Android应用中实现手势识别,并提供相应的源代码示例。通过以上步骤,你已经完成了Android应用中手势识别的基本实现。你可以根据需要添加更多的手势回调方法,并在每个方法中实现自定义的逻辑。确保将"your_view_id"替换为你要添加手势识别功能的视图的ID。希望本文对你在Android应用开发中实现手势识别有所帮助!

HackMasterX的博客 234

android 绘制手势轨迹,先来了解下手势动作在Android上的应用,如在屏幕上画个圈。。。...

该楼层疑似违规已被系统折叠隐藏此楼查看此楼package com.example.gesturebuilderdemo;import java.io.File;import java.util.ArrayList;import java.util.List;import java.util.Set;import android.os.Bundle;import android.os.Enviro...

weixin_34526733的博客 449

20、Android 开发中的手势识别与碎片使用

本文详细介绍了Android开发中的手势识别与碎片使用。通过Gesture Builder和ScaleGestureDetector类,可以实现自定义手势和捏合手势的检测;同时,碎片(Fragment)作为构建动态用户界面的重要组件,支持通过布局文件或代码添加、管理,并实现了碎片之间的灵活通信。文章还提供了一个完整的示例应用,演示了如何在Android Studio中创建和使用碎片,并实现碎片之间的数据交互。

115

手势识别3:Android实现手部检测和手势识别(可实时运行,含Android源码)

开发一个Android手势识别,手势动作识别,手势数据集,手势识别APP,YOLO手势识别;APP在普通Android手机上可以达到实时的手势识别效果,CPU(4线程)约30ms左右,GPU约25ms左右 ,基本满足业务的性能需求

AI吃大瓜的博客 1万+

基于百度AI实现Android隔空手势识别完整开发指南

手势识别技术正逐步改变人机交互方式,尤其在无接触场景中展现出巨大潜力。Android平台通过结合传感器、摄像头和AI算法,实现了对隔空手势的识别与响应。本章将从基本概念入手,解析其技术构成与交互逻辑,并探讨其在智能设备、车载系统、AR/VR等领域的典型应用场景。同时,我们将理解为何在当前的移动开发中,集成手势识别能力已成为提升用户体验的重要方向,为后续章节中的开发实践打下坚实基础。

weixin_33240461的博客 917

腾讯会议视频过期了怎么办?亲测可用:用这个免费插件抢救你的会议录制与回放

本文提供了三种亲测有效的腾讯会议视频过期恢复方案,包括浏览器缓存提取、专业工具组合及预防性措施。特别推荐使用免费插件抢救过期会议录制与回放,帮助职场人解决重要视频资料丢失的难题。

weixin_33686714的博客 264

Android手势识别GestureDetector和ScaleGestureDetector介绍与使用,以自定义一个可拖拽拉伸的ImageView为例

GestureDetector主要用于检测单指手势,例如单击、长按、滑动等,不支持多指手势。用于检测缩放手势,即双指捏合或者扩张的手势。它提供了 onScale() 和 onScaleBegin() 等回调方法来处理缩放手势的开始、进行中和结束时的事件。首先,我们可以直接继承 ImageView,并通过 Matrix 来控制图片的移动和拉伸。其次,使用 GestureDetector 监听移动的相关事件,使用 ScaleGestureDetector 监听拉伸的相关事件。

阿飞的博客 3868

20231126-超详细Isaac Gym安装教程(基于双系统版本)

我一开始使用了WSL2虚拟环境试了集群服务器里面的交互式桌面,但是都失败了,WSL2因为没办法调用系统的GPU,应该有办法配置但我放弃了,感兴趣的可以自己探索(图片贴出来了)。服务器上出现的问题是Segmentation fault (core dumped),目前还没解决,有可能是驱动和cuda版本的问题(后面有时间再解决)等上图右边都达到了100%,就创建了一个名为rlgpu的conda环境,然后会出现success。注意安装版本要低于3.9!参考链接:非常详细,大家直接看这个博主的安装就行。

m0_37802038的博客 2万+

如何解决 Android Activity底部手势提示线导致页面高度变化?

首先, 请确保你的布局文件中的根布局节点使用了 android:fitsSystemWindows="true" 属性。这将使布局能够调整自己的尺寸以适应底部导航栏。 如果你的布局仍然出现问题, 你可以尝试在代码中使用以下方法来更新布局: View rootView = findViewById(R.id.root_view); rootView.requestLayout(); 这应该使布局重...

weixin_35756373的博客 982

WKWebView 白屏问题,WKWebView request body丢失问题

1,WKWebView 白屏问题 WKWebView 自诩拥有更快的加载速度,更低的内存占用,但实际上 WKWebView 是一个多进程组件,Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候,我们也惊讶于打开 WKWebView 后,App 进程内存消耗反而大幅下降,但是仔细观察会发现,Other Process 的内存占用会...

weixin_37543822的博客 737

PaddleOCR实战:从零开始打造专属OCR模型的完整指南

本文提供了一份从零开始使用PaddleOCR打造专属OCR模型的完整实战指南。内容涵盖环境搭建、数据标注与准备、模型训练与调优,以及最终的模型转换与部署。重点介绍了如何利用PaddleOCR进行模型训练,并整合PPOCRLabel等工具高效准备数据集,帮助开发者快速构建适用于特定场景的高精度OCR识别能力。

rl6adventurer的博客 733

安卓手势识别开发浅谈

手势识别在安卓开发里虽然用到的场景不是很多,但有时候还是能让你的产品更方便,更有趣味。比如登陆时不用每次重复输入密码,只需九宫格手势解锁,就像支付宝客户端那样。下面我们完成一个简单的手势识别开发。 我们首先需要一个描述手势动作的文件,在模拟器里预装一个叫GesturesBuilder的程序,这个程序就是让你创建自己的手势的(GesturesBuilder的源代码在sdk问samples里面有,有...

大军的输出 267

pcle接口详解&用途说明

它原来的名称为“3GIO”,由英特尔在2001年提出。pcel x16插槽 用途用于显卡、RAID阵列卡,向下兼容PCIE x1/x4/x8/x16。(PCIE x16插槽长89mm,有164根针脚,靠主板外侧端有一卡口,将16x分为前后两组,短插槽有22根针脚,主要用于供电,长插槽142根,主要用于数据传输,具有16通道所带来的高带宽)PCI-E x8插槽插槽用途:全长56mm,有98根针脚,与PCI-E x16比较,主要是数据针脚减少至76根,短的供电针脚仍然是22针脚。

qq_43289307的博客 1万+

Android开发——用户在屏幕上的手势识别

0.前言转载请注明出处:http://blog.youkuaiyun.com/seu_calvin/article/details/52462493Android开发中,我们常常需要获取用户的手势操作事件,从而回调相应的方法完成我们的逻辑业务,在讲手势识别之前,我们有必要了解一下View相关的位置属性。1.View位置属性View是一种界面上控件的一种抽象,代表了一个控件。View有很多位置属性。...

关于Android开发的一些技术点总结 ╮( ̄▽ ̄”)╭ 1万+

系统架构设计师【第1章】: 绪论 (核心总结)

系统架构 (System Architecture)是系统的一种整体的高层次的结构表示,是系统的骨架和根基,也决定了系统的健壮性和生命周期的长短。系统架构设计师是承担系统架构设计的核心角色,他不仅是连接用户需求和系统进一步设计与实现的桥梁,也是系统开发早期阶段质量保证的关键角色。通俗地说,系统架构 (System Architecture)

数据知道的博客 1万+

BF8815A

了解ADBMS1818芯片的人会发现,这款BF8815B和1818有着很多相似的东西,当你了解其中一款AFE之后,另一款你的上手也会特别快,下面文章是我针对1818梳理的一些重要知识点,此篇文章主要讲解BF8815B的一些差异点。

m0_61973119的博客 561

android开发之手势识别

android开发之手势识别

江南一点雨的专栏 8034

全志V3S嵌入式驱动开发(驱动开发准备)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 之前的文章都是教大家怎么搭建环境、看原理图、编译内核和根文件系统、做镜像,直到现在才进入驱动开发的主题。毕竟整个专栏的目的,还是希望大家能够学会驱动外部硬件。驱动好硬件,分成硬件和软件两个部分,硬件的部分比较好理解,就是看相关的信号有没有对接上即可,但是软件对接、甚至于怎么写软件,大家好像接触的比较少,所以这个时候,就可以简单讨论一下,怎么样才能写驱动代码。 1、了解外部驱动芯

嵌入式-老费,一个分享专业嵌入式知识的blog 2025

android 手势开发,Android开发之——手势识别

前言使用浏览器或者输入法应用时会有一些手势操作,还可以自定义手势。这些神奇的操作是怎么做的呢?这一篇重点记录手势的识别和创建。这篇的内容使用到了android.gesture包,具体的例子参考的是Sample中的GestureBuilder程序。手势识别的一般步骤手势创建手势创建主要用到了GestureOverlayView和GestureLibrary。GestrueOverlayView的父类...

weixin_35711852的博客 836

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

android手势监听功能吗,Android开发中实现手势识别功能的方法

Android开发中实现手势识别功能的方法发布时间:2020-11-20 15:57:35来源:亿速云阅读:121作者:LeahAndroid开发中实现手势识别功能的方法?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。触摸交互中的概念常用事件首先要了解一些常用的事件:ACTION_DOWN:第一个手指按下ACTION_UP:第一个手指抬...

weixin_39854369的博客 242

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Kafka Streams Processor API 深度指南:自定义处理器、状态存储与 Topology 组装

本文基于 Apache Kafka 仓库中 [docs/streams/developer-guide/processor-api.md](https://link.gitcode.com/i/ac4c497d7ee59eab3aa3025ec2e3af3a) 展开,系统讲解 Kafka Streams 的 **Processor API(低层流处理 API)**:如何通过实现 `Processo

gitblog_00741的博客 871

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

android 红外 手势识别,android开发之手势识别

由于精确度等原因,手势识别在android中用的并不多,不过这并不妨碍我们来玩玩这个神奇的玩意。在android中要使用手势,先得建立手势库,建立手势库非常简单,新建一个android sample project,建一个android示例工程,然后选择创建的android版本,完了之后看到这个界面:选择gesturebuilder,创建成功之后把它安装到真机上,然后可以在里边添加手势,并给手势命...

weixin_35461942的博客 376

MediaPipe框架在Android端的手势识别应用开发指南

本文详细介绍了如何在Android应用中集成MediaPipe框架实现实时手势识别。通过环境搭建、核心代码实现及性能优化等步骤,指导开发者利用MediaPipe的高效管道和预训练模型,快速构建可精准检测21个手部关键点的应用,适用于AR交互、智能控制等多种场景。

weixin_29048309的博客 233
上一篇: Android开发之导入错误
下一篇: 移动互联网时代的9大赚钱机会
minimicall
博客等级 码龄18年 407粉丝 174原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值