Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

วิธีเพิ่มฟีเจอร์ Shake เพื่อ Refresh แอป Android

ภาพรวม

บทความนี้สาธิตวิธีเพิ่มฟีเจอร์ "Shake to Refresh" (เขยิบโทรศัพท์เพื่อรีเฟรช) ใน Android โดยใช้ SensorManager และ Sensor.TYPE_ACCELEROMETER พร้อมอัลกอริทึมตรวจจับการเขยิบที่แม่นยำ จัดการ Lifecycle ให้ถูกต้อง และป้องกัน False Positive

ขั้นตอนที่ 1: สร้างโปรเจกต์ใหม่

สร้างโปรเจกต์ Android ใหม่ใน Android Studio

ขั้นตอนที่ 2: Layout (res/layout/activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center">

    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="📱 เขยิบโทรศัพท์เพื่อ Refresh"
        android:textSize="20sp"
        android:textStyle="bold"
        android:gravity="center" />

    <TextView
        android:id="@+id/tvLastShake"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="ยังไม่มีการเขยิบ"
        android:textSize="14sp"
        android:gravity="center" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:visibility="gone" />

</LinearLayout>

ขั้นตอนที่ 3: ShakeDetector Helper Class (แยก Logic ให้สะอาด)

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;

public class ShakeDetector implements SensorEventListener {

    public interface OnShakeListener {
        void onShakeDetected();
    }

    private static final float SHAKE_THRESHOLD_GRAVITY = 2.7f; // ค่าความเร่งขั้นต่ำ (G-force)
    private static final int SHAKE_SLOP_TIME_MS = 500; // เวลาขั้นต่ำระหว่าง Shake (ms)
    private static final int SHAKE_COUNT_RESET_TIME_MS = 3000; // รีเซ็ตนับหลัง 3 วิ

    private final OnShakeListener listener;
    private long lastShakeTime = 0;
    private int shakeCount = 0;
    private long lastShakeTimestamp = 0;

    public ShakeDetector(OnShakeListener listener) {
        this.listener = listener;
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) return;

        float x = event.values[0];
        float y = event.values[1];
        float z = event.values[2];

        // คำนวณ Acceleration Magnitude (ลบ Gravity ออก)
        float gX = x / SensorManager.GRAVITY_EARTH;
        float gY = y / SensorManager.GRAVITY_EARTH;
        float gZ = z / SensorManager.GRAVITY_EARTH;

        // Magnitude = sqrt(x² + y² + z²) - 1 (ลบ 1G ของแรงโน้มถ่วง)
        float magnitude = (float) Math.sqrt(gX * gX + gY * gY + gZ * gZ) - 1f;

        if (magnitude > SHAKE_THRESHOLD_GRAVITY) {
            long now = System.currentTimeMillis();
            
            // Debounce: ไม่นับซ้ำภายใน SHAKE_SLOP_TIME_MS
            if (now - lastShakeTime < SHAKE_SLOP_TIME_MS) {
                return;
            }

            // นับจำนวน Shake ติดกัน
            if (now - lastShakeTimestamp < SHAKE_COUNT_RESET_TIME_MS) {
                shakeCount++;
            } else {
                shakeCount = 1;
            }
            lastShakeTimestamp = now;
            lastShakeTime = now;

            // Trigger เมื่อเขยิบ 2 ครั้งติดกัน (ป้องกัน False Positive)
            if (shakeCount >= 2) {
                shakeCount = 0;
                if (listener != null) {
                    listener.onShakeDetected();
                }
            }
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // ไม่ใช้
    }
}

ขั้นตอนที่ 4: MainActivity.java

import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class MainActivity extends AppCompatActivity {

    private SensorManager sensorManager;
    private Sensor accelerometer;
    private ShakeDetector shakeDetector;
    private TextView tvStatus, tvLastShake;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tvStatus = findViewById(R.id.tvStatus);
        tvLastShake = findViewById(R.id.tvLastShake);

        // Initialize Sensor
        sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

        if (accelerometer == null) {
            tvStatus.setText("❌ อุปกรณ์ไม่มี Accelerometer");
            return;
        }

        // Setup Shake Detector
        shakeDetector = new ShakeDetector(() -> {
            runOnUiThread(this::onShakeDetected);
        });
    }

    private void onShakeDetected() {
        // UI Update
        String time = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(new Date());
        tvLastShake.setText("เขยิบล่าสุด: " + time);
        tvStatus.setText("✅ ตรวจจับ Shake! กำลัง Refresh...");

        // Simulate Refresh Action
        performRefresh();

        // Haptic Feedback
        // performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    }

    private void performRefresh() {
        // TODO: ใส่ Logic Refresh จริงที่นี่
        // เช่น: SwipeRefreshLayout.setRefreshing(true) -> Load Data -> setRefreshing(false)
        
        new android.os.Handler().postDelayed(() -> {
            runOnUiThread(() -> {
                tvStatus.setText("📱 เขยิบโทรศัพท์เพื่อ Refresh");
                Toast.makeText(this, "Refreshed!", Toast.LENGTH_SHORT).show();
            });
        }, 1500);
    }

    @Override
    protected void onResume() {
        super.onResume();
        // Register Listener เมื่อ App กลับมาหน้าจอ
        if (accelerometer != null) {
            sensorManager.registerListener(shakeDetector, accelerometer, SensorManager.SENSOR_DELAY_UI);
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        // Unregister เมื่อ App ไม่อยู่หน้าจอ (ประหยัด Battery)
        sensorManager.unregisterListener(shakeDetector);
    }
}

ขั้นตอนที่ 5: AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="app.com.sample">

    <uses-feature android:name="android.hardware.sensor.accelerometer" android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

การปรับแต่งความไว (Sensitivity Tuning)

การตั้งค่าค่าเริ่มต้นผลกระทบ
SHAKE_THRESHOLD_GRAVITY2.7fลด = ไวขึ้น, เพิ่ม = ต้องเขยิบแรงขึ้น
SHAKE_SLOP_TIME_MS500เวลาขั้นต่ำระหว่างการ Detect ครั้งละครั้ง
SHAKE_COUNT_RESET_TIME_MS3000เวลารีเซ็ตนับ Shake ติดกัน
Required Shake Count2จำนวนครั้งที่ต้องเขยิบติดกัน (1 = ไวมาก, 3 = ปลอดภัย)

Sensor Delay Options

  • SENSOR_DELAY_NORMAL (~200ms/5Hz) - ใช้สำหรับ Screen Orientation, ประหยัด Battery
  • SENSOR_DELAY_UI (~60ms/16Hz) - แนะนำสำหรับ Shake Detection สมดุลดี
  • SENSOR_DELAY_GAME (~20ms/50Hz) - ใช้สำหรับ Game, Responsive มาก
  • SENSOR_DELAY_FASTEST (~0ms/Max) - ใช้ Battery มากที่สุด

ปัญหาที่พบบ่อยและวิธีแก้

ปัญหาสาเหตุวิธีแก้
Detect ผิดบ่อย (False Positive)Threshold ต่ำเกิน / ไม่มี Debounceเพิ่ม Threshold, เพิ่ม Slop Time, ต้อง Shake 2 ครั้งติดกัน
ไม่ Detect (False Negative)Threshold สูงเกิน / Sensor Delay ช้าลด Threshold, ใช้ SENSOR_DELAY_UI หรือ GAME
Battery DrainRegister Listener ตลอดเวลาUnregister ใน onPause(), ใช้ SENSOR_DELAY_UI
Conflict กับ ScrollShake ขณะ Scroll ListDisable Shake เมื่อ ListView/RecyclerView กำลัง Scroll (OnScrollListener)

ตัวเลือกอื่น: ใช้ Library

สำหรับ Production แนะนำ Library ที่ทดสอบ済み:

  • ShakeDetector (Square): com.squareup:shakedetector:1.0.0 - เรียบง่าย ทำงานดี
  • ShakeListener (GitHub Gist): ไฟล์เดียว Copy-Paste ได้
  • Android-Shake-Library: Feature-rich, Configurable

Use Cases จริง

  • Pull-to-Refresh Alternative: เขยิบแทน Swipe Down
  • Dice Roller: เขยิบทอยเต๋า
  • Emergency SOS: เขยิบแรง/นาน → ส่ง SOS
  • Game Input: ควบคุมเกมด้วยการเขยิบ
  • Debug Menu: เขยิบ 3 ครั้ง → เปิด Developer Options