Compare commits

..

2 Commits
main ... anyh

BIN
.DS_Store vendored

Binary file not shown.

@ -1,2 +1,2 @@
#Wed Jun 04 10:47:17 CST 2025
#Fri Apr 25 19:53:19 CST 2025
gradle.version=8.5

@ -1,2 +1,7 @@
#Thu Jun 05 00:28:15 CST 2025
java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home
<<<<<<< HEAD
#Mon May 26 14:33:47 GMT+08:00 2025
java.home=D\:\\Android\\AS_INstall\\jbr
=======
#Sat May 24 23:39:46 CST 2025
java.home=D\:\\Andr\\jbr
>>>>>>> a11e684a7b53b2288bd2435ed63bc73bcd82db22

Binary file not shown.

@ -1 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidTestResultsUserPreferences">
<option name="androidTestResultsTableState">
<map>
<entry key="975774096">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Medium_Phone_API_36" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1074918448">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Medium_Phone_API_36" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1125150954">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Medium_Phone_API_36" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
</map>
</option>
</component>
</project>

@ -1,751 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceStreaming">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Sony" />
<option name="codename" value="A402SO" />
<option name="id" value="A402SO" />
<option name="labId" value="google" />
<option name="manufacturer" value="Sony" />
<option name="name" value="Xperia 10" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2520" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="labId" value="google" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OnePlus" />
<option name="codename" value="OP535DL1" />
<option name="id" value="OP535DL1" />
<option name="labId" value="google" />
<option name="manufacturer" value="OnePlus" />
<option name="name" value="CPH2409" />
<option name="screenDensity" value="401" />
<option name="screenX" value="1080" />
<option name="screenY" value="2412" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OnePlus" />
<option name="codename" value="OP5552L1" />
<option name="id" value="OP5552L1" />
<option name="labId" value="google" />
<option name="manufacturer" value="OnePlus" />
<option name="name" value="CPH2415" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2412" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
<option name="codename" value="OP573DL1" />
<option name="id" value="OP573DL1" />
<option name="labId" value="google" />
<option name="manufacturer" value="OPPO" />
<option name="name" value="CPH2557" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="labId" value="google" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a15" />
<option name="id" value="a15" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A15" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a15x" />
<option name="id" value="a15x" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A15 5G" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a16x" />
<option name="id" value="a16x" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A16 5G" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a35x" />
<option name="id" value="a35x" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A35" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="arcfox" />
<option name="id" value="arcfox" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="razr plus 2024" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1080" />
<option name="screenY" value="1272" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="austin" />
<option name="id" value="austin" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g 5G (2022)" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="b6q" />
<option name="id" value="b6q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Flip 6" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1080" />
<option name="screenY" value="2640" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="default" value="true" />
<option name="id" value="comet" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="default" value="true" />
<option name="id" value="comet" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm2q" />
<option name="id" value="dm2q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="S23 Plus" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="dubai" />
<option name="id" value="dubai" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="edge 30" />
<option name="screenDensity" value="405" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="default" value="true" />
<option name="id" value="e1q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e3q" />
<option name="id" value="e3q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24 Ultra" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1440" />
<option name="screenY" value="3120" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="eos" />
<option name="id" value="eos" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Eos" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="fogona" />
<option name="id" value="fogona" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g play - 2024" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="g0q" />
<option name="id" value="g0q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-S906U1" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gta9pwifi" />
<option name="id" value="gta9pwifi" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-X210" />
<option name="screenDensity" value="240" />
<option name="screenX" value="1200" />
<option name="screenY" value="1920" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts7xllite" />
<option name="id" value="gts7xllite" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-T738U" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="formFactor" value="Tablet" />
<option name="id" value="gts8uwifi" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8wifi" />
<option name="formFactor" value="Tablet" />
<option name="id" value="gts8wifi" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8" />
<option name="screenDensity" value="274" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts9fe" />
<option name="id" value="gts9fe" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S9 FE 5G" />
<option name="screenDensity" value="280" />
<option name="screenX" value="1440" />
<option name="screenY" value="2304" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts9wifi" />
<option name="id" value="gts9wifi" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-X710" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="maui" />
<option name="id" value="maui" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g play - 2023" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="o1q" />
<option name="id" value="o1q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21" />
<option name="screenDensity" value="421" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="samsung" />
<option name="codename" value="pa3q" />
<option name="id" value="pa3q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S25 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3120" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="formFactor" value="Wear OS" />
<option name="id" value="r11" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="r11q" />
<option name="id" value="r11q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-S711U" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="t2q" />
<option name="id" value="t2q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21 Plus" />
<option name="screenDensity" value="394" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="formFactor" value="Tablet" />
<option name="id" value="tangorpro" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="tegu" />
<option name="id" value="tegu" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="default" value="true" />
<option name="id" value="tokay" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="default" value="true" />
<option name="id" value="tokay" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="xcover7" />
<option name="id" value="xcover7" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-G556B" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2408" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>

@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

@ -4,15 +4,24 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-06-03T12:40:50.122129Z">
<DropdownSelection timestamp="2025-05-26T13:46:32.570753600Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=YPL0223227003230" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=83784f3e" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="MovenetLightningTest">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="MovenetThunderTest">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="MovenetMultiPoseTest">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

@ -1,11 +1,10 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
android {
compileSdkVersion 31
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
@ -50,18 +49,11 @@ dependencies {
implementation 'org.tensorflow:tensorflow-lite:2.14.0'
implementation 'org.tensorflow:tensorflow-lite-gpu:2.5.0'
implementation 'org.tensorflow:tensorflow-lite-support:0.3.0'
// Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
// Room
def room_version = "2.4.3"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation 'org.mindrot:jbcrypt:0.4'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

@ -3,7 +3,6 @@
package="org.tensorflow.lite.examples.poseestimation">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
@ -20,7 +19,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainTabActivity" />
<activity android:name=".MainActivity" />
<activity android:name=".GenderSelectionActivity" />
<activity android:name=".AgeSelectionActivity" />
@ -38,20 +36,6 @@
<activity
android:name=".SignupActivity"
android:exported="false" />
<activity android:name=".ExerciseDetailActivity" />
<activity android:name=".EditProfileActivity"/>
<activity android:name=".VideoAnalysisActivity"/>
<activity android:name=".VideoAnalysisResultActivity"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

@ -18,7 +18,6 @@ class AgeSelectionActivity : AppCompatActivity() {
private lateinit var nextButton: MaterialButton
private lateinit var backButton: ImageButton
private var selectedGender: String? = null
private var username: String? = null
private var currentAge = 25
private val minAge = 12
@ -32,7 +31,6 @@ class AgeSelectionActivity : AppCompatActivity() {
setContentView(R.layout.activity_age_selection)
selectedGender = intent.getStringExtra("selected_gender")
username = intent.getStringExtra("username")
selectedAgeText = findViewById(R.id.selectedAgeText)
age1Above = findViewById(R.id.age1Above)
@ -84,7 +82,6 @@ class AgeSelectionActivity : AppCompatActivity() {
val intent = Intent(this, WeightSelectionActivity::class.java)
intent.putExtra("selected_gender", selectedGender)
intent.putExtra("selected_age", currentAge)
intent.putExtra("username", username)
startActivity(intent)
finish()
}

@ -1,77 +0,0 @@
package org.tensorflow.lite.examples.poseestimation
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
class DataFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: VideoAnalysisAdapter
private var currentUsername: String? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
android.util.Log.d("DataFragment", "onCreateView called")
return inflater.inflate(R.layout.fragment_data, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
android.util.Log.d("DataFragment", "onViewCreated called")
// 获取当前登录用户名
val prefs = requireContext().getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
currentUsername = prefs.getString("username", null)?.trim()
android.util.Log.d("DataFragment", "Current Username: $currentUsername")
if (currentUsername == null) {
Toast.makeText(requireContext(), "未登录,无法查看数据", Toast.LENGTH_SHORT).show()
android.util.Log.w("DataFragment", "Current username is null, cannot load data.")
return
}
recyclerView = view.findViewById(R.id.recycler_view_video_analysis_results)
recyclerView.layoutManager = LinearLayoutManager(context)
loadVideoAnalysisResults()
}
private fun loadVideoAnalysisResults() {
android.util.Log.d("DataFragment", "loadVideoAnalysisResults called for username: $currentUsername")
currentUsername?.let { username ->
lifecycleScope.launch {
val db = AppDatabase.getDatabase(requireContext())
val results = withContext(Dispatchers.IO) {
db.videoAnalysisResultDao().getVideoAnalysisResultsByUsername(username).firstOrNull()
}
withContext(Dispatchers.Main) {
if (results != null && results.isNotEmpty()) {
android.util.Log.d("DataFragment", "Loaded ${results.size} video analysis results.")
adapter = VideoAnalysisAdapter(results)
recyclerView.adapter = adapter
} else {
// 没有数据,可以显示一个提示
Toast.makeText(requireContext(), "暂无训练记录", Toast.LENGTH_SHORT).show()
android.util.Log.d("DataFragment", "No video analysis results found for username: $username")
}
}
}
}
}
}

@ -1,252 +0,0 @@
package org.tensorflow.lite.examples.poseestimation
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.widget.EditText
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
import org.tensorflow.lite.examples.poseestimation.data.UserProfile
class EditProfileActivity : AppCompatActivity() {
private lateinit var editUsername: EditText
private lateinit var editSignature: EditText
private lateinit var imageAvatar: ImageView
private lateinit var btnSelectPhoto: ImageView
private lateinit var btnSave: com.google.android.material.button.MaterialButton
private lateinit var btnBack: ImageView
// 新增的个人信息输入框
private lateinit var editGender: EditText
private lateinit var editAge: EditText
private lateinit var editWeight: EditText
private lateinit var editHeight: EditText
private var currentUsername: String? = null
private var selectedImageUri: Uri? = null // 存储复制到内部存储后的URI
// 用于请求READ_EXTERNAL_STORAGE权限
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
isGranted: Boolean ->
if (isGranted) {
openGallery()
} else {
Toast.makeText(this, "需要读取存储权限才能选择头像", Toast.LENGTH_SHORT).show()
}
}
// 用于启动相册选择图片并获取结果
private val selectPhotoLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val imageUri = result.data?.data
imageUri?.let {
// 在ImageView中显示选中的图片
imageAvatar.setImageURI(it)
// 将图片复制到内部存储
lifecycleScope.launch(Dispatchers.IO) {
val internalUri = copyImageToInternalStorage(it)
withContext(Dispatchers.Main) {
selectedImageUri = internalUri
android.util.Log.d("EditProfileActivity", "Image copied to internal storage: $internalUri")
}
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edit_profile)
// 获取当前登录用户名
val prefs = getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
currentUsername = prefs.getString("username", null)
// 如果没有获取到用户名,说明未登录,提示并关闭页面
if (currentUsername == null) {
Toast.makeText(this, "未登录,无法编辑资料", Toast.LENGTH_SHORT).show()
finish()
return
}
// 绑定布局中的控件
editUsername = findViewById(R.id.edit_username)
editSignature = findViewById(R.id.edit_signature)
imageAvatar = findViewById(R.id.image_avatar)
btnSelectPhoto = findViewById(R.id.btn_select_photo)
btnSave = findViewById(R.id.btn_save)
btnBack = findViewById(R.id.btn_back)
// 绑定新增的个人信息输入框
editGender = findViewById(R.id.edit_gender)
editAge = findViewById(R.id.edit_age)
editWeight = findViewById(R.id.edit_weight)
editHeight = findViewById(R.id.edit_height)
// 加载并显示当前用户信息
loadUserProfile()
// 设置选择头像按钮的点击事件
btnSelectPhoto.setOnClickListener {
checkPermissionAndOpenGallery()
}
// 设置保存按钮的点击事件
btnSave.setOnClickListener {
saveProfile()
}
// 设置返回按钮的点击事件
btnBack.setOnClickListener {
finish() // 结束当前Activity返回上一个Activity即SettingFragment所在的Activity
}
}
// 从数据库加载用户数据并显示
private fun loadUserProfile() {
currentUsername?.let { username ->
lifecycleScope.launch(Dispatchers.IO) {
val db = AppDatabase.getDatabase(this@EditProfileActivity)
// 使用getUserByUsername查询User对象其中包含signature字段
val user = db.userDao().getUserByUsername(username).first()
// 查询UserProfile数据
val userProfile = db.userProfileDao().getUserProfileByUsername(username).firstOrNull()
withContext(Dispatchers.Main) {
if (user != null) {
editUsername.setText(user.username)
editSignature.setText(user.signature)
// 加载用户头像的逻辑
userProfile?.avatarUri?.let { uriString ->
try {
val imageUri = Uri.parse(uriString)
imageAvatar.setImageURI(imageUri)
selectedImageUri = imageUri // 确保 selectedImageUri 也更新以防再次保存
} catch (e: Exception) {
android.util.Log.e("EditProfileActivity", "Error parsing avatar URI: $uriString", e)
}
}
} else {
// 用户不存在,这通常不应该发生如果已经登录
Toast.makeText(this@EditProfileActivity, "加载用户信息失败", Toast.LENGTH_SHORT).show()
}
// 显示UserProfile数据
if (userProfile != null) {
editGender.setText(userProfile.gender)
// 对于 Int 类型,需要转换为 String
if (userProfile.age != 0) editAge.setText(userProfile.age.toString())
if (userProfile.weight != 0) editWeight.setText(userProfile.weight.toString())
if (userProfile.height != 0) editHeight.setText(userProfile.height.toString())
}
}
}
}
}
// 打开相册选择图片
private fun openGallery() {
val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
selectPhotoLauncher.launch(galleryIntent)
}
// 检查权限并打开相册
private fun checkPermissionAndOpenGallery() {
when {
ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED -> {
// 权限已授予
openGallery()
}
shouldShowRequestPermissionRationale(android.Manifest.permission.READ_EXTERNAL_STORAGE) -> {
// 向用户解释为什么需要这个权限,然后再次请求
Toast.makeText(this, "需要读取存储权限来选择您的头像", Toast.LENGTH_LONG).show()
requestPermissionLauncher.launch(android.Manifest.permission.READ_EXTERNAL_STORAGE)
}
else -> {
// 请求权限
requestPermissionLauncher.launch(android.Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
}
// 将图片复制到应用内部存储并返回新URI
private fun copyImageToInternalStorage(uri: Uri): Uri? {
return try {
val inputStream = contentResolver.openInputStream(uri)
val outputFileName = "avatar_${System.currentTimeMillis()}.jpg"
val outputFile = java.io.File(filesDir, outputFileName)
val outputStream = outputFile.outputStream()
inputStream?.copyTo(outputStream)
inputStream?.close()
outputStream.close()
android.util.Log.d("EditProfileActivity", "Image copied to: ${outputFile.absolutePath}")
Uri.fromFile(outputFile)
} catch (e: Exception) {
android.util.Log.e("EditProfileActivity", "Error copying image to internal storage: ${e.message}", e)
null
}
}
// 保存修改后的个人资料
private fun saveProfile() {
currentUsername?.let { username ->
val newSignature = editSignature.text.toString().trim()
// 获取个人信息输入框的值
val newGender = editGender.text.toString().trim()
val newAge = editAge.text.toString().trim().toIntOrNull() ?: 0
val newWeight = editWeight.text.toString().trim().toIntOrNull() ?: 0
val newHeight = editHeight.text.toString().trim().toIntOrNull() ?: 0
// 添加日志输出,确认获取到的数据和当前用户名
android.util.Log.d("EditProfileActivity", "Current Username: $username")
android.util.Log.d("EditProfileActivity", "New Signature: $newSignature")
android.util.Log.d("EditProfileActivity", "New Gender: $newGender, Age: $newAge, Weight: $newWeight, Height: $newHeight")
android.util.Log.d("EditProfileActivity", "Selected Image URI before save: $selectedImageUri")
lifecycleScope.launch(Dispatchers.IO) {
val db = AppDatabase.getDatabase(this@EditProfileActivity)
// 调用UserDao中的updateSignature方法更新个性签名
db.userDao().updateSignature(username, newSignature)
// 更新UserProfile表中的数据包括头像URI
val updatedProfile = UserProfile(username, newGender, newAge, newWeight, newHeight, selectedImageUri?.toString())
android.util.Log.d("EditProfileActivity", "Attempting to update UserProfile: $updatedProfile")
val rowsUpdated = db.userProfileDao().updateUserProfile(updatedProfile)
android.util.Log.d("EditProfileActivity", "UserProfile rows updated: $rowsUpdated for username: $username") // 记录更新的行数
withContext(Dispatchers.Main) {
if (rowsUpdated > 0) {
Toast.makeText(this@EditProfileActivity, "资料保存成功", Toast.LENGTH_SHORT).show()
setResult(Activity.RESULT_OK)
finish()
} else {
Toast.makeText(this@EditProfileActivity, "资料保存失败:未找到对应用户档案", Toast.LENGTH_LONG).show()
}
}
}
}
}
}

@ -1,70 +0,0 @@
package org.tensorflow.lite.examples.poseestimation
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import android.widget.Button
import android.view.View
class ExerciseDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_exercise_detail)
// 找到布局中的控件
val backButton = findViewById<ImageButton>(R.id.back_button)
val exerciseImage = findViewById<ImageView>(R.id.exercise_detail_image)
val exerciseName = findViewById<TextView>(R.id.exercise_detail_name)
val exerciseDescription = findViewById<TextView>(R.id.exercise_detail_description)
val startTrainingButton = findViewById<Button>(R.id.start_training_button)
// 获取从 HomeFragment 传递过来的动作名称
val exerciseNameFromIntent = intent.getStringExtra("exercise_name")
// 设置页面内容
exerciseNameFromIntent?.let { name ->
exerciseName.text = name
// 根据动作名称设置对应的图片和描述
val imageResId = when (name) {
"硬拉" -> R.drawable.deadlift
"深蹲" -> R.drawable.deep_squats
"平板支撑" -> R.drawable.plank
"引体向上" -> R.drawable.pull_up
"俯卧撑" -> R.drawable.push_up
else -> R.drawable.placeholder_image
}
exerciseImage.setImageResource(imageResId)
val descriptionText = when (name) {
"硬拉" -> "硬拉是锻炼下背部、臀部和腿部后侧链肌群的重要力量训练动作,开始时双脚与肩同宽或略窄,脚尖略微外展,杠铃置于胫骨中上段前方,双手采用正握或一正一反握法,宽度略宽于肩,背部保持自然挺直,胸部抬起,下背部收紧,避免弓背或塌腰,动作启动时臀部稍下降,膝盖微屈,通过脚掌发力\"推地\",将杠铃沿小腿贴近身体的方向上拉,直至身体完全伸直,站起时肩膀向后收拢,但不要过度后仰,放下时先屈髋再屈膝,保持背部紧绷,控制杠铃缓慢下降,整个过程中保持重心在脚中部,呼吸节奏为提起时呼气,放下时吸气,硬拉强调臀腿力量带动全身发力,而不是单纯弯腰提重物,新手应从空杆开始,掌握正确动作模式后再逐渐增加重量,以避免受伤。"
"深蹲" -> "深蹲是提升下肢力量和肌肉耐力的重要训练动作,主要锻炼股四头肌、臀大肌、腘绳肌及核心肌群。进行深蹲时,双脚与肩同宽或略宽,脚尖略微外展,双手可伸直前平举、抱头或交叉胸前以保持平衡,屈髋屈膝缓慢下蹲,想象臀部向后下方坐下,膝盖弯曲至大腿与地面平行或更低,过程中始终保持背部挺直、核心收紧,避免膝盖内扣或过度前伸,脚跟始终贴地发力,起身时脚掌踩实地面,臀部向前上方收缩,将身体推回起始位置,整个动作应控制节奏,避免借力或猛冲,呼吸上通常是在下蹲时吸气,起身时呼气,初学者建议从徒手开始,掌握正确姿势后再逐步增加负重,确保动作标准以避免受伤。"
"平板支撑" -> "平板支撑是一项强化核心肌群的经典训练动作主要锻炼腹部、下背部和骨盆周围的稳定肌群能够有效提升身体的整体控制力和平衡能力。进行时应以小臂和脚尖作为支撑点手肘位于肩膀正下方前臂与上臂呈90度夹角身体从头到脚踝保持一条直线避免臀部抬得过高或塌陷下去防止出现弓背或塌腰的现象。整个过程中要始终保持核心收紧尤其是腹部发力帮助维持身体稳定同时注意呼吸节奏不要憋气通常采用均匀的腹式呼吸方式。双脚并拢或略微分开脚尖着地以增加稳定性身体重心落在躯干中部避免用手臂或肩膀过多承重。初学者可以从20-30秒开始随着力量增强逐渐延长时间但应以动作标准为前提避免因坚持时间过长而牺牲姿势正确性。坚持练习可以明显提升核心力量、改善体态并为其他运动动作打下良好的基础。"
"引体向上" -> "引体向上是锻炼背部、手臂和核心肌群的重要复合动作,主要依靠背阔肌发力带动身体上升。开始时双手宽握单杠,手掌朝外,身体自然下垂,双脚可交叉或并拢,保持稳定。动作过程中,利用背部力量将身体向上拉起,避免借助惯性或摆动,肩胛骨收紧下沉,胸部尽量靠近单杠,同时保持核心收紧,避免腰部过度晃动或弓背、塌腰。在最高点稍作停顿后,缓慢控制身体下降至手臂完全伸直,不要突然放松或耸肩。整个过程以背阔肌为主导,肱二头肌辅助发力,初学者若力量不足可借助弹力带辅助或进行反向慢速下落来逐步提升力量,同时注意动作节奏与呼吸配合,上拉时呼气,下降时吸气,确保动作标准比完成次数更重要,以避免受伤并有效激活目标肌群。"
"俯卧撑" -> "俯卧撑是一项经典的自重训练动作主要锻炼胸大肌、三角肌前束和肱三头肌同时也能强化核心稳定性。进行时双手略宽于肩撑地手指朝前或微外展身体从头到脚呈一条直线避免塌腰或翘臀核心肌群始终保持收紧状态。下降过程中控制身体缓慢下压肘部向后约45度弯曲胸部接近地面但不贴地随后以手掌为支点推起身体回到起始位置注意保持动作稳定不要借助惯性或弓背塌腰。呼吸节奏应配合动作下落时吸气推起时呼气避免憋气影响发力。初学者可根据自身力量选择跪姿俯卧撑或斜式俯卧撑作为过渡逐步提升至标准动作。俯卧撑的关键在于动作质量而非数量保持正确姿势不仅能提高训练效果还能有效预防受伤。坚持练习可增强上肢力量、提升身体协调性和耐力是居家锻炼不可或缺的基础动作之一。"
// TODO: 添加其他运动的描述
else -> "暂无详细描述。"
}
exerciseDescription.text = descriptionText
// 显示描述 TextView
exerciseDescription.visibility = View.VISIBLE
}
// 设置返回按钮点击事件
backButton.setOnClickListener {
onBackPressed() // 返回上一个Activity
}
// 设置开始训练按钮点击事件,跳转到姿态识别页面
startTrainingButton.setOnClickListener {
val intent = Intent(this, VideoAnalysisActivity::class.java)
// 传递当前动作名称给 MainActivity
intent.putExtra("current_exercise", exerciseNameFromIntent)
startActivity(intent)
}
}
}

@ -14,14 +14,11 @@ class GenderSelectionActivity : AppCompatActivity() {
private lateinit var femaleText: TextView
private lateinit var nextButton: MaterialButton
private var selectedGender: String? = null
private var username: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gender_selection)
username = intent.getStringExtra("username")
maleButton = findViewById(R.id.maleButton)
femaleButton = findViewById(R.id.femaleButton)
maleText = findViewById(R.id.maleText)
@ -41,9 +38,9 @@ class GenderSelectionActivity : AppCompatActivity() {
}
nextButton.setOnClickListener {
// 跳转到年龄选择页面,并传递性别信息
val intent = Intent(this, AgeSelectionActivity::class.java)
intent.putExtra("selected_gender", selectedGender)
intent.putExtra("username", username)
startActivity(intent)
}
}

@ -8,11 +8,6 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.Dispatchers
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
import org.tensorflow.lite.examples.poseestimation.data.UserProfile
import kotlin.math.abs
class HeightSelectionActivity : AppCompatActivity() {
@ -27,7 +22,6 @@ class HeightSelectionActivity : AppCompatActivity() {
private var selectedGender: String? = null
private var selectedAge: Int = 0
private var selectedWeight: Int = 0
private var username: String? = null
private var currentHeight = 167
private val minHeight = 100
@ -43,10 +37,7 @@ class HeightSelectionActivity : AppCompatActivity() {
selectedGender = intent.getStringExtra("selected_gender")
selectedAge = intent.getIntExtra("selected_age", 0)
selectedWeight = intent.getIntExtra("selected_weight", 0)
username = intent.getStringExtra("username")?.trim()
android.util.Log.d("HeightSelectionActivity", "Received username: $username")
selectedHeightText = findViewById(R.id.selectedHeightText)
heightUnit = findViewById(R.id.heightUnit)
height1Above = findViewById(R.id.height1Above)
@ -93,36 +84,11 @@ class HeightSelectionActivity : AppCompatActivity() {
private fun setupClickListeners() {
nextButton.setOnClickListener {
// 保存用户个人信息到数据库
val db = AppDatabase.getDatabase(this)
val gender = selectedGender ?: ""
val age = selectedAge
val weight = selectedWeight
val height = currentHeight
val user = username ?: ""
android.util.Log.d("HeightSelectionActivity", "Attempting to save profile for username: $user, gender: $gender, age: $age, weight: $weight, height: $height")
lifecycleScope.launch(Dispatchers.IO) {
try {
val profile = UserProfile(user, gender, age, weight, height)
android.util.Log.d("HeightSelectionActivity", "Saving UserProfile: $profile")
val rowsUpdated = db.userProfileDao().updateUserProfile(profile)
android.util.Log.d("HeightSelectionActivity", "UserProfile rows updated: $rowsUpdated for username: $user")
if (rowsUpdated == 0) {
db.userProfileDao().insertUserProfile(profile)
android.util.Log.d("HeightSelectionActivity", "UserProfile inserted after update failed for username: $user")
} else {
android.util.Log.d("HeightSelectionActivity", "UserProfile updated successfully for username: $user")
}
} catch (e: Exception) {
android.util.Log.e("HeightSelectionActivity", "Error saving user profile for username: $user: ${e.message}", e)
}
}
val intent = Intent(this, LoginActivity::class.java)
intent.putExtra("selected_gender", selectedGender)
intent.putExtra("selected_age", selectedAge)
intent.putExtra("selected_weight", selectedWeight)
intent.putExtra("selected_height", currentHeight)
intent.putExtra("username", username)
startActivity(intent)
finish()
}
@ -132,7 +98,6 @@ class HeightSelectionActivity : AppCompatActivity() {
intent.putExtra("selected_gender", selectedGender)
intent.putExtra("selected_age", selectedAge)
intent.putExtra("selected_weight", selectedWeight)
intent.putExtra("username", username)
startActivity(intent)
finish()
}

@ -1,80 +0,0 @@
package org.tensorflow.lite.examples.poseestimation
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.Fragment
import org.tensorflow.lite.examples.poseestimation.R
class HomeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 设置硬拉卡片
val deadliftCard = view.findViewById<View>(R.id.deadlift_card)
val deadliftImage = deadliftCard.findViewById<ImageView>(R.id.exercise_image)
val deadliftName = deadliftCard.findViewById<TextView>(R.id.exercise_name)
deadliftImage.setImageResource(R.drawable.deadlift)
deadliftName.text = "硬拉"
deadliftCard.setOnClickListener {
startExerciseDetail("硬拉")
}
// 设置深蹲卡片
val squatCard = view.findViewById<View>(R.id.squat_card)
val squatImage = squatCard.findViewById<ImageView>(R.id.exercise_image)
val squatName = squatCard.findViewById<TextView>(R.id.exercise_name)
squatImage.setImageResource(R.drawable.deep_squats)
squatName.text = "深蹲"
squatCard.setOnClickListener {
startExerciseDetail("深蹲")
}
// 设置平板支撑卡片
val plankCard = view.findViewById<View>(R.id.plank_card)
val plankImage = plankCard.findViewById<ImageView>(R.id.exercise_image)
val plankName = plankCard.findViewById<TextView>(R.id.exercise_name)
plankImage.setImageResource(R.drawable.plank)
plankName.text = "平板支撑"
plankCard.setOnClickListener {
startExerciseDetail("平板支撑")
}
// 设置引体向上卡片
val pullupCard = view.findViewById<View>(R.id.pullup_card)
val pullupImage = pullupCard.findViewById<ImageView>(R.id.exercise_image)
val pullupName = pullupCard.findViewById<TextView>(R.id.exercise_name)
pullupImage.setImageResource(R.drawable.pull_up)
pullupName.text = "引体向上"
pullupCard.setOnClickListener {
startExerciseDetail("引体向上")
}
// 设置俯卧撑卡片
val pushupCard = view.findViewById<View>(R.id.pushup_card)
val pushupImage = pushupCard.findViewById<ImageView>(R.id.exercise_image)
val pushupName = pushupCard.findViewById<TextView>(R.id.exercise_name)
pushupImage.setImageResource(R.drawable.push_up)
pushupName.text = "俯卧撑"
pushupCard.setOnClickListener {
startExerciseDetail("俯卧撑")
}
}
private fun startExerciseDetail(exerciseName: String) {
val intent = Intent(requireContext(), ExerciseDetailActivity::class.java)
intent.putExtra("exercise_name", exerciseName)
startActivity(intent)
}
}

@ -2,21 +2,24 @@ package org.tensorflow.lite.examples.poseestimation
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import android.widget.Button
import android.widget.TextView
import android.widget.EditText
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import org.tensorflow.lite.examples.poseestimation.auth.UserDao
import org.tensorflow.lite.examples.poseestimation.utils.PasswordUtils
class LoginActivity : AppCompatActivity() {
private lateinit var userDao: UserDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
// 初始化 UserDao
userDao = UserDao(this)
// 获取从上一个页面传递的数据
val selectedGender = intent.getStringExtra("selected_gender")
val selectedAge = intent.getIntExtra("selected_age", 0)
@ -30,16 +33,13 @@ class LoginActivity : AppCompatActivity() {
val forgotPassword = findViewById<TextView>(R.id.forgotPassword)
val signupTab = findViewById<TextView>(R.id.tabSignup)
// 获取数据库实例
val db = AppDatabase.getDatabase(this)
// 登录按钮点击事件
loginBtn.setOnClickListener {
val username = emailEdit.text.toString().trim()
val email = emailEdit.text.toString().trim()
val password = passwordEdit.text.toString().trim()
// 表单验证
if (username.isEmpty()) {
if (email.isEmpty()) {
emailEdit.error = "请输入用户名"
return@setOnClickListener
}
@ -48,33 +48,44 @@ class LoginActivity : AppCompatActivity() {
return@setOnClickListener
}
// 在协程中执行数据库操作
lifecycleScope.launch {
try {
val user = db.userDao().getUser(username, password).first()
if (user != null) {
// 登录成功
val prefs = getSharedPreferences("user_prefs", MODE_PRIVATE)
prefs.edit().putString("username", username).apply()
val intent = Intent(this@LoginActivity, org.tensorflow.lite.examples.poseestimation.MainTabActivity::class.java)
// 执行数据库查询验证
val user = userDao.getUserByUsername(email)
if (user != null) {
// 验证密码
if (PasswordUtils.verifyPassword(password, user.passwordHash)) {
// 登录成功
Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show()
val intent = Intent(this, MainActivity::class.java)
intent.putExtra("selected_gender", selectedGender)
intent.putExtra("selected_age", selectedAge)
intent.putExtra("selected_weight", selectedWeight)
intent.putExtra("selected_height", selectedHeight)
//intent.putExtra("user_id", user.id) // 传递用户ID
startActivity(intent)
finish()
} else {
runOnUiThread {
Toast.makeText(this@LoginActivity, "用户名或密码错误", Toast.LENGTH_SHORT).show()
}
//密码错误
passwordEdit.error = "密码不正确"
}
} catch (e: Exception) {
runOnUiThread {
Toast.makeText(this@LoginActivity, "登录失败:${e.message}", Toast.LENGTH_SHORT).show()
}
}
}else {
//用户不存在
emailEdit.error = "用户不存在"
}
}
// TODO: 这里添加实际的登录验证逻辑
// 目前仅做演示,直接跳转到主页面
// val intent = Intent(this, MainActivity::class.java)
// intent.putExtra("selected_gender", selectedGender)
// intent.putExtra("selected_age", selectedAge)
// intent.putExtra("selected_weight", selectedWeight)
// intent.putExtra("selected_height", selectedHeight)
// startActivity(intent)
// finish()
}
// 忘记密码点击事件
forgotPassword.setOnClickListener {

@ -48,9 +48,6 @@ import org.tensorflow.lite.examples.poseestimation.ml.PoseClassifier
import org.tensorflow.lite.examples.poseestimation.ml.PoseNet
import org.tensorflow.lite.examples.poseestimation.ml.TrackerType
import org.tensorflow.lite.examples.poseestimation.ml.Type
import org.tensorflow.lite.examples.poseestimation.data.Angle
import org.tensorflow.lite.examples.poseestimation.data.AngleDifference
import org.tensorflow.lite.examples.poseestimation.data.Person
class MainActivity : AppCompatActivity() {
companion object {
@ -144,17 +141,11 @@ class MainActivity : AppCompatActivity() {
isPoseClassifier()
}
private lateinit var tvPoseAdvice: TextView
private var currentExerciseType: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// keep screen on while app is running
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
currentExerciseType = intent.getStringExtra("current_exercise")
tvScore = findViewById(R.id.tvScore)
tvFPS = findViewById(R.id.tvFps)
spnModel = findViewById(R.id.spnModel)
@ -167,8 +158,6 @@ class MainActivity : AppCompatActivity() {
tvClassificationValue3 = findViewById(R.id.tvClassificationValue3)
swClassification = findViewById(R.id.swPoseClassification)
vClassificationOption = findViewById(R.id.vClassificationOption)
tvPoseAdvice = findViewById(R.id.tv_pose_advice)
initSpinner()
spnModel.setSelection(modelPos)
swClassification.setOnCheckedChangeListener(setClassificationListener)
@ -214,8 +203,7 @@ class MainActivity : AppCompatActivity() {
override fun onDetectedInfo(
personScore: Float?,
poseLabels: List<Pair<String, Float>>?,
persons: List<Person>
poseLabels: List<Pair<String, Float>>?
) {
tvScore.text = getString(R.string.tfe_pe_tv_score, personScore ?: 0f)
poseLabels?.sortedByDescending { it.second }?.let {
@ -232,29 +220,6 @@ class MainActivity : AppCompatActivity() {
convertPoseLabels(if (it.size >= 3) it[2] else null)
)
}
if (persons.isNotEmpty() && currentExerciseType != null) {
val angleObj = Angle()
val angleDifference = AngleDifference(angleObj)
val personKeyPoints = persons[0].keyPoints
val advice = when (currentExerciseType) {
"硬拉" -> angleDifference.calculateAngleDifference(personKeyPoints, "2")
"深蹲" -> angleDifference.calculateAngleDifference(personKeyPoints, "4")
"平板支撑" -> angleDifference.calculateAngleDifference(personKeyPoints, "5")
"引体向上" -> angleDifference.calculateAngleDifference(personKeyPoints, "1")
"俯卧撑" -> angleDifference.calculateAngleDifference(personKeyPoints, "3")
else -> "未知动作类型,无法给出建议"
}
runOnUiThread {
tvPoseAdvice.text = advice
}
} else if (currentExerciseType == null) {
runOnUiThread {
tvPoseAdvice.text = "未指定动作类型,无法给出建议"
}
}
}
}).apply {

@ -1,80 +0,0 @@
package org.tensorflow.lite.examples.poseestimation
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.appcompat.widget.Toolbar
import android.widget.TextView
import org.tensorflow.lite.examples.poseestimation.R
class MainTabActivity : AppCompatActivity() {
private lateinit var navHome: ImageView
private lateinit var navData: ImageView
private lateinit var navSetting: ImageView
private lateinit var toolbar: Toolbar
private lateinit var toolbarTitle: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_tab)
toolbar = findViewById(R.id.toolbar)
toolbarTitle = findViewById(R.id.toolbar_title)
navHome = findViewById(R.id.nav_home)
navData = findViewById(R.id.nav_data)
navSetting = findViewById(R.id.nav_setting)
// 底部导航栏跳转逻辑:
// 1. 在 onCreate 中初始化三个导航按钮navHome、navData、navSetting
// 2. 默认显示 HomeFragment并更新导航图标
// 3. 为每个导航按钮设置点击事件,点击时调用 switchFragment 切换对应的 Fragment并调用 updateNavIcons 更新图标状态
// 4. switchFragment 方法使用 supportFragmentManager 的 beginTransaction 替换 fragment_container 中的 Fragment
// 5. updateNavIcons 方法根据当前选中的导航项0、1、2更新三个导航按钮的图标资源
// 默认显示HomeFragment
switchFragment(HomeFragment(), "形动力")
updateNavIcons(0)
navHome.setOnClickListener {
switchFragment(HomeFragment(), "形动力")
updateNavIcons(0)
}
navData.setOnClickListener {
switchFragment(DataFragment(), "形动力")
updateNavIcons(1)
}
navSetting.setOnClickListener {
switchFragment(SettingFragment(), "形动力")
updateNavIcons(2)
}
}
private fun switchFragment(fragment: Fragment, title: String) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit()
toolbarTitle.text = title
}
private fun updateNavIcons(selected: Int) {
when (selected) {
0 -> {
navHome.setImageResource(R.drawable.home1)
navData.setImageResource(R.drawable.data2)
navSetting.setImageResource(R.drawable.setting2)
}
1 -> {
navHome.setImageResource(R.drawable.home2)
navData.setImageResource(R.drawable.data1)
navSetting.setImageResource(R.drawable.setting2)
}
2 -> {
navHome.setImageResource(R.drawable.home2)
navData.setImageResource(R.drawable.data2)
navSetting.setImageResource(R.drawable.setting1)
}
}
}
}

@ -15,14 +15,11 @@ class Onboarding3Fragment : Fragment() {
): View? {
val view = inflater.inflate(R.layout.activity_onboarding3, container, false)
val username = arguments?.getString("username") // 获取用户名
// 找到Start now按钮并设置点击事件
val startButton = view.findViewById<FrameLayout>(R.id.small_butto_container)
startButton.setOnClickListener {
// 跳转到性别选择页面
val intent = Intent(requireActivity(), GenderSelectionActivity::class.java)
intent.putExtra("username", username) // 传递用户名
startActivity(intent)
requireActivity().finish() // 结束当前的OnboardingActivity
}

@ -9,9 +9,7 @@ class OnboardingActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_onboarding)
val username = intent.getStringExtra("username")
val viewPager = findViewById<ViewPager2>(R.id.viewPager)
viewPager.adapter = OnboardingAdapter(this, username)
viewPager.adapter = OnboardingAdapter(this)
}
}

@ -1,23 +1,18 @@
package org.tensorflow.lite.examples.poseestimation
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class OnboardingAdapter(activity: FragmentActivity, private val username: String?) : FragmentStateAdapter(activity) {
class OnboardingAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment {
val fragment = when (position) {
return when (position) {
0 -> Onboarding1Fragment()
1 -> Onboarding2Fragment()
2 -> Onboarding3Fragment()
else -> Onboarding1Fragment()
}
fragment.arguments = Bundle().apply {
putString("username", username)
}
return fragment
}
}

@ -1,234 +0,0 @@
package org.tensorflow.lite.examples.poseestimation
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
import org.tensorflow.lite.examples.poseestimation.data.UserProfile
class SettingFragment : Fragment() {
private var isPersonalInfoExpanded = false
private var isMyDataExpanded = false
private lateinit var tvUsername: TextView
private lateinit var tvSignature: TextView
private lateinit var btnPersonalInfo: TextView
private lateinit var btnLogout: TextView
private lateinit var layoutPersonalDetail: View
private lateinit var tvGender: TextView
private lateinit var tvAge: TextView
private lateinit var tvWeight: TextView
private lateinit var tvHeight: TextView
private lateinit var btnAccountInfo: TextView
private lateinit var imageAvatar: ImageView
private lateinit var tvTotalTrainings: TextView
private lateinit var tvTotalCalories: TextView
private lateinit var tvTotalTime: TextView
private lateinit var btnMyData: TextView
private lateinit var layoutMyData: View
private var currentUsername: String? = null
// 注册用于启动EditProfileActivity并处理结果的Launcher
private val editProfileResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// 如果EditProfileActivity返回成功结果重新加载用户数据
loadUserData()
// 如果个人信息当前是展开的,也重新加载个人详细信息
if (isPersonalInfoExpanded) {
loadPersonalInfo()
}
// 重新加载我的数据
loadMyData()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_setting, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val context = requireContext()
val db = AppDatabase.getDatabase(context)
// 获取当前登录用户名假设用SharedPreferences存储
val prefs = context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
currentUsername = prefs.getString("username", null)?.trim()
if (currentUsername == null) {
Toast.makeText(context, "未登录,请重新登录", Toast.LENGTH_SHORT).show()
startActivity(Intent(context, LoginActivity::class.java))
requireActivity().finish()
return
}
// 绑定控件
tvUsername = view.findViewById(R.id.tv_username)
tvSignature = view.findViewById(R.id.tv_signature)
btnPersonalInfo = view.findViewById(R.id.btn_personal_info)
btnLogout = view.findViewById(R.id.btn_logout)
layoutPersonalDetail = view.findViewById(R.id.layout_personal_detail)
tvGender = view.findViewById(R.id.tv_gender)
tvAge = view.findViewById(R.id.tv_age)
tvWeight = view.findViewById(R.id.tv_weight)
tvHeight = view.findViewById(R.id.tv_height)
btnAccountInfo = view.findViewById(R.id.btn_account_info)
imageAvatar = view.findViewById(R.id.image_avatar)
tvTotalTrainings = view.findViewById(R.id.tv_total_trainings)
tvTotalCalories = view.findViewById(R.id.tv_total_calories)
tvTotalTime = view.findViewById(R.id.tv_total_time)
btnMyData = view.findViewById(R.id.btn_my_data)
layoutMyData = view.findViewById(R.id.layout_my_data)
// 页面创建时加载并显示用户数据
loadUserData()
// 页面创建时加载并显示我的数据
layoutMyData.visibility = if (isMyDataExpanded) View.VISIBLE else View.GONE
// 账户信息点击事件使用Launcher启动EditProfileActivity
btnAccountInfo.setOnClickListener {
val intent = Intent(requireContext(), EditProfileActivity::class.java)
editProfileResultLauncher.launch(intent)
}
// 个人信息展开/收起
btnPersonalInfo.setOnClickListener {
isPersonalInfoExpanded = !isPersonalInfoExpanded
layoutPersonalDetail.visibility = if (isPersonalInfoExpanded) View.VISIBLE else View.GONE
if (isPersonalInfoExpanded) {
// 展开时加载个人详细信息
loadPersonalInfo()
}
}
// 我的数据展开/收起
btnMyData.setOnClickListener {
isMyDataExpanded = !isMyDataExpanded
layoutMyData.visibility = if (isMyDataExpanded) View.VISIBLE else View.GONE
if (isMyDataExpanded) {
loadMyData()
}
}
// 注销
btnLogout.setOnClickListener {
prefs.edit().remove("username").apply()
startActivity(Intent(context, LoginActivity::class.java))
requireActivity().finish()
}
}
// 提取加载用户名和签名的逻辑
private fun loadUserData() {
currentUsername?.let { username ->
lifecycleScope.launch {
val db = AppDatabase.getDatabase(requireContext())
// 使用 Flow 查询,并在协程中收集第一个结果
val user = withContext(Dispatchers.IO) {
db.userDao().getUserByUsername(username).first()
}
// 获取UserProfile数据
val userProfile = withContext(Dispatchers.IO) {
db.userProfileDao().getUserProfileByUsername(username).firstOrNull()
}
// 在主线程更新UI
withContext(Dispatchers.Main) {
tvUsername.text = user?.username ?: "-"
tvSignature.text = user?.signature ?: "这个人很懒,什么都没写"
// 加载并显示用户头像
userProfile?.avatarUri?.let { uriString ->
android.util.Log.d("SettingFragment", "Attempting to load avatar from URI: $uriString")
try {
val imageUri = Uri.parse(uriString)
imageAvatar.setImageURI(imageUri)
android.util.Log.d("SettingFragment", "Avatar loaded successfully from URI: $imageUri")
} catch (e: Exception) {
// 处理URI无效或图片加载失败的情况可以显示默认头像或日志
android.util.Log.e("SettingFragment", "Error loading avatar from URI: $uriString", e)
imageAvatar.setImageResource(R.drawable.placeholder_image) // 显示默认头像
}
} ?: run { // 如果avatarUri为null也显示默认头像
android.util.Log.d("SettingFragment", "Avatar URI is null, displaying placeholder.")
imageAvatar.setImageResource(R.drawable.placeholder_image)
}
}
}
}
}
// 提取加载个人详细信息的逻辑
private fun loadPersonalInfo() {
currentUsername?.let { username ->
lifecycleScope.launch {
val db = AppDatabase.getDatabase(requireContext())
// 使用 Flow 进行单次查询
val profile = withContext(Dispatchers.IO) {
db.userProfileDao().getUserProfileByUsername(username).firstOrNull()
}
// 在主线程更新UI
withContext(Dispatchers.Main) {
if (profile != null) {
tvGender.text = "性别:${profile.gender}"
tvAge.text = "年龄:${profile.age}"
tvWeight.text = "体重:${profile.weight} kg"
tvHeight.text = "身高:${profile.height} cm"
// 添加日志输出
android.util.Log.d("SettingFragment", "UserProfile loaded: $profile")
} else {
tvGender.text = "性别:-"
tvAge.text = "年龄:-"
tvWeight.text = "体重:-"
tvHeight.text = "身高:-"
// 添加日志输出
android.util.Log.d("SettingFragment", "UserProfile not found for username: $username")
}
}
}
}
}
// 提取加载我的数据的逻辑
private fun loadMyData() {
currentUsername?.let { username ->
lifecycleScope.launch {
val db = AppDatabase.getDatabase(requireContext())
val videoAnalysisResults = withContext(Dispatchers.IO) {
db.videoAnalysisResultDao().getVideoAnalysisResultsByUsername(username).first()
}
withContext(Dispatchers.Main) {
val totalTrainings = videoAnalysisResults.size
// 简化的卡路里和时间估算 (假设每次训练消耗50kcal每次训练平均5分钟)
val totalCalories = totalTrainings * 50
val totalMinutes = totalTrainings * 5
tvTotalTrainings.text = "总训练次数:$totalTrainings"
tvTotalCalories.text = "总消耗卡路里:$totalCalories kcal"
tvTotalTime.text = "总训练时间:$totalMinutes 分钟"
}
}
}
}
}

@ -1,25 +1,23 @@
package org.tensorflow.lite.examples.poseestimation
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
import org.tensorflow.lite.examples.poseestimation.data.User
import org.tensorflow.lite.examples.poseestimation.data.UserProfile
import org.tensorflow.lite.examples.poseestimation.auth.UserDao
import org.tensorflow.lite.examples.poseestimation.auth.User
import org.tensorflow.lite.examples.poseestimation.utils.PasswordUtils
class SignupActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_signup)
// 初始化 UserDao
var userDao = UserDao(this)
// 绑定控件
val usernameEdit = findViewById<EditText>(R.id.editUsername)
val passwordEdit = findViewById<EditText>(R.id.editPassword)
@ -27,9 +25,6 @@ class SignupActivity : AppCompatActivity() {
val signupBtn = findViewById<Button>(R.id.btnSignup)
val loginTab = findViewById<TextView>(R.id.tabLogin)
// 获取数据库实例
val db = AppDatabase.getDatabase(this)
// 注册按钮点击事件
signupBtn.setOnClickListener {
val username = usernameEdit.text.toString().trim()
@ -57,41 +52,28 @@ class SignupActivity : AppCompatActivity() {
return@setOnClickListener
}
// 在IO线程中执行数据库操作
lifecycleScope.launch(Dispatchers.IO) {
try {
// 检查用户名是否已存在
val existingUser = db.userDao().getUserByUsername(username).first()
if (existingUser != null) {
runOnUiThread {
Toast.makeText(this@SignupActivity, "用户名已存在", Toast.LENGTH_SHORT).show()
}
return@launch
}
// 检查用户名是否已存在
if (userDao.checkUsernameExists(username)) {
usernameEdit.error = "用户名已存在"
Toast.makeText(this, "用户名已存在", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
// 加密密码
val passwordHash = PasswordUtils.hashPassword(password)
val user = User()
user.setUsername(username)
user.setPasswordHash(passwordHash)
// 创建新用户
val newUser = User(username, password)
db.userDao().insertUser(newUser)
android.util.Log.d("SignupActivity", "User inserted: $username")
// 同步插入user_profiles表默认值
val newProfile = UserProfile(username, "-", 0, 0, 0)
val profileId = db.userProfileDao().insertUserProfile(newProfile)
android.util.Log.d("SignupActivity", "UserProfile inserted for username: $username with ID: $profileId")
runOnUiThread {
Toast.makeText(this@SignupActivity, "注册成功", Toast.LENGTH_SHORT).show()
// 注册成功后跳转到OnboardingActivity
val intent = Intent(this@SignupActivity, OnboardingActivity::class.java)
intent.putExtra("username", username) // 传递用户名
startActivity(intent)
finish() // 结束注册界面
}
} catch (e: Exception) {
runOnUiThread {
Toast.makeText(this@SignupActivity, "注册失败:${e.message}", Toast.LENGTH_SHORT).show()
}
}
// 保存到数据库
val userId = userDao.addUser(user)
if (userId != (-1).toLong()) {
Toast.makeText(this, "注册成功", Toast.LENGTH_SHORT).show()
finish()
} else {
Toast.makeText(this, "注册失败,请重试", Toast.LENGTH_SHORT).show()
}
}

@ -41,9 +41,9 @@ class SplashActivity : AppCompatActivity() {
vector2.layoutParams = params2
}
// 2秒后跳转到登录
// 2秒后跳转到引导
Handler(Looper.getMainLooper()).postDelayed({
startActivity(Intent(this, LoginActivity::class.java))
startActivity(Intent(this, OnboardingActivity::class.java))
finish()
}, 2000)
}

@ -1,286 +0,0 @@
package org.tensorflow.lite.examples.poseestimation
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.delay
import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
import org.tensorflow.lite.examples.poseestimation.data.VideoAnalysisResult
import org.tensorflow.lite.examples.poseestimation.ml.MoveNet
import org.tensorflow.lite.examples.poseestimation.ml.PoseDetector
import org.tensorflow.lite.examples.poseestimation.ml.ModelType
import org.tensorflow.lite.examples.poseestimation.data.Device
import org.tensorflow.lite.examples.poseestimation.data.Person
import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
import org.tensorflow.lite.examples.poseestimation.evaluator.DeadliftEvaluator
import org.tensorflow.lite.examples.poseestimation.evaluator.SquatEvaluator
import org.tensorflow.lite.examples.poseestimation.evaluator.ExerciseEvaluator
import org.tensorflow.lite.examples.poseestimation.evaluator.PlankEvaluator
import org.tensorflow.lite.examples.poseestimation.evaluator.PullUpEvaluator
import org.tensorflow.lite.examples.poseestimation.evaluator.PushUpEvaluator
class VideoAnalysisActivity : AppCompatActivity() {
private lateinit var tvExerciseName: TextView
private lateinit var btnSelectVideo: Button
private lateinit var tvAnalysisStatus: TextView
private lateinit var progressBarAnalysis: ProgressBar
private var currentExerciseType: String? = null
private var currentUsername: String? = null
private lateinit var poseDetector: PoseDetector
// 用于存储复制到内部存储后的视频URI
private var internalVideoUri: Uri? = null
// Launcher for selecting video from local storage
private val selectVideoLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val videoUri: Uri? = result.data?.data
videoUri?.let {
// 将视频复制到内部存储
lifecycleScope.launch(Dispatchers.IO) {
val copiedUri = copyVideoToInternalStorage(it)
withContext(Dispatchers.Main) {
if (copiedUri != null) {
internalVideoUri = copiedUri
android.util.Log.d("VideoAnalysisActivity", "视频已复制到内部存储: $internalVideoUri")
tvAnalysisStatus.text = "已选择视频: ${getFileName(it)},开始分析..."
tvAnalysisStatus.visibility = View.VISIBLE
progressBarAnalysis.visibility = View.VISIBLE
progressBarAnalysis.progress = 0
startVideoAnalysis(internalVideoUri!!, currentExerciseType)
} else {
Toast.makeText(this@VideoAnalysisActivity, "视频复制失败,请重试", Toast.LENGTH_SHORT).show()
tvAnalysisStatus.text = "视频复制失败"
tvAnalysisStatus.visibility = View.VISIBLE
progressBarAnalysis.visibility = View.GONE
}
}
}
} ?: run {
Toast.makeText(this, "未选择视频", Toast.LENGTH_SHORT).show()
tvAnalysisStatus.text = "未选择视频"
tvAnalysisStatus.visibility = View.VISIBLE
progressBarAnalysis.visibility = View.GONE
}
} else {
Toast.makeText(this, "取消选择视频", Toast.LENGTH_SHORT).show()
tvAnalysisStatus.text = "取消选择视频"
tvAnalysisStatus.visibility = View.VISIBLE
progressBarAnalysis.visibility = View.GONE
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_analysis)
// 获取当前登录用户名 (假设用SharedPreferences存储)
val prefs = getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
currentUsername = prefs.getString("username", null)?.trim()
if (currentUsername == null) {
Toast.makeText(this, "未登录,请重新登录", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, LoginActivity::class.java))
finish()
return
}
// Get exercise name from intent
currentExerciseType = intent.getStringExtra("current_exercise")
// Initialize views
tvExerciseName = findViewById(R.id.tv_exercise_name)
btnSelectVideo = findViewById(R.id.btn_select_video)
tvAnalysisStatus = findViewById(R.id.tv_analysis_status)
progressBarAnalysis = findViewById(R.id.progress_bar_analysis)
// Initialize PoseDetector (using MoveNet Lightning by default)
poseDetector = MoveNet.create(this, Device.CPU, ModelType.Lightning)
// Set exercise name
tvExerciseName.text = currentExerciseType ?: "未知动作"
// Set click listener for video selection button
btnSelectVideo.setOnClickListener {
openVideoPicker()
}
}
// Override onDestroy to close the poseDetector
override fun onDestroy() {
super.onDestroy()
poseDetector.close()
}
private fun openVideoPicker() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "video/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
selectVideoLauncher.launch(Intent.createChooser(intent, "选择视频"))
}
// Helper function to get file name from Uri
private fun getFileName(uri: Uri): String {
var result: String? = null
if (uri.scheme == "content") {
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1) {
result = it.getString(nameIndex)
}
}
}
}
if (result == null) {
result = uri.path
val cut = result?.lastIndexOf('/')
if (cut != -1) {
result = result?.substring(cut!! + 1)
}
}
return result ?: "未知文件"
}
// 将视频复制到应用内部存储并返回新URI
private fun copyVideoToInternalStorage(uri: Uri): Uri? {
return try {
val inputStream = contentResolver.openInputStream(uri)
val outputFileName = "video_${System.currentTimeMillis()}.mp4"
val outputFile = java.io.File(filesDir, outputFileName)
val outputStream = outputFile.outputStream()
inputStream?.copyTo(outputStream)
inputStream?.close()
outputStream.close()
android.util.Log.d("VideoAnalysisActivity", "视频已复制到: ${outputFile.absolutePath}")
Uri.fromFile(outputFile)
} catch (e: Exception) {
android.util.Log.e("VideoAnalysisActivity", "复制视频到内部存储出错: ${e.message}", e)
null
}
}
// Placeholder for video analysis logic
private fun startVideoAnalysis(videoUri: Uri, exerciseType: String?) {
val db = AppDatabase.getDatabase(applicationContext)
val videoAnalysisResultDao = db.videoAnalysisResultDao()
lifecycleScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
tvAnalysisStatus.text = "正在初始化姿态识别模型..."
btnSelectVideo.isEnabled = false // Disable button during analysis
}
val retriever = MediaMetadataRetriever()
retriever.setDataSource(applicationContext, videoUri)
val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0
// Process frames at a reasonable interval, e.g., 10 frames per second for analysis
val frameRate = 10 // frames per second
val frameIntervalUs = 1_000_000L / frameRate // interval in microseconds
var currentFrameTimeUs = 0L
// Initialize evaluator based on exercise type
val evaluator: ExerciseEvaluator? = when (exerciseType) {
"硬拉" -> DeadliftEvaluator()
"深蹲" -> SquatEvaluator()
"平板支撑" -> PlankEvaluator()
"引体向上" -> PullUpEvaluator()
"俯卧撑" -> PushUpEvaluator()
// TODO: Add evaluators for other exercises
else -> null
}
while (currentFrameTimeUs <= durationMs * 1000) { // durationMs is in milliseconds, getFrameAtTime expects microseconds
val bitmap = retriever.getFrameAtTime(currentFrameTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
bitmap?.let { frameBitmap ->
val persons = poseDetector.estimatePoses(frameBitmap)
val person = persons.firstOrNull()
person?.let { p ->
if (p.score > 0.3) { // Only consider poses with reasonable confidence
// Pass keypoints to the evaluator
evaluator?.evaluateFrame(p.keyPoints)
}
}
frameBitmap.recycle() // Recycle bitmap to free memory
}
val progress = ((currentFrameTimeUs.toFloat() / (durationMs * 1000)) * 100).toInt()
withContext(Dispatchers.Main) {
progressBarAnalysis.progress = progress
tvAnalysisStatus.text = "正在分析视频帧: ${currentFrameTimeUs / 1000}ms / ${durationMs}ms"
}
currentFrameTimeUs += frameIntervalUs
}
retriever.release()
withContext(Dispatchers.Main) {
tvAnalysisStatus.text = "姿态识别完成,正在评估..."
}
// --- Pose Evaluation and Scoring ---
val finalScore: Float
val evaluation: String
if (evaluator != null) {
finalScore = evaluator.getFinalScore()
evaluation = evaluator.getFinalEvaluation(finalScore)
} else {
finalScore = 0f
evaluation = "暂不支持该动作的详细评估。"
}
// Store result in database
val result = VideoAnalysisResult(
username = currentUsername ?: "未知用户",
exerciseType = exerciseType ?: "未知运动",
videoUri = videoUri.toString(), // 使用已经复制到内部存储的URI
score = finalScore,
evaluation = evaluation,
timestamp = System.currentTimeMillis()
)
videoAnalysisResultDao.insertVideoAnalysisResult(result)
withContext(Dispatchers.Main) {
tvAnalysisStatus.text = "分析完成!正在显示结果..."
progressBarAnalysis.visibility = View.GONE
btnSelectVideo.isEnabled = true // Re-enable button
val intent = Intent(this@VideoAnalysisActivity, VideoAnalysisResultActivity::class.java).apply {
putExtra("exercise_name", currentExerciseType)
putExtra("score", finalScore)
putExtra("evaluation", evaluation)
}
startActivity(intent)
finish() // Finish this activity so user can't go back to it
}
}
}
}

@ -1,71 +0,0 @@
package org.tensorflow.lite.examples.poseestimation
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import androidx.core.content.FileProvider
import org.tensorflow.lite.examples.poseestimation.data.VideoAnalysisResult
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class VideoAnalysisAdapter(private val results: List<VideoAnalysisResult>) : RecyclerView.Adapter<VideoAnalysisAdapter.VideoAnalysisViewHolder>() {
class VideoAnalysisViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvExerciseType: TextView = itemView.findViewById(R.id.tv_exercise_type)
val tvScore: TextView = itemView.findViewById(R.id.tv_score)
val tvEvaluation: TextView = itemView.findViewById(R.id.tv_evaluation)
val videoThumbnail: ImageView = itemView.findViewById(R.id.video_thumbnail)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoAnalysisViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_video_analysis_card, parent, false)
return VideoAnalysisViewHolder(view)
}
override fun onBindViewHolder(holder: VideoAnalysisViewHolder, position: Int) {
val result = results[position]
holder.tvExerciseType.text = "运动类型:${result.exerciseType}"
holder.tvScore.text = "${result.score.toInt()}"
holder.tvEvaluation.text = "评价:${result.evaluation}"
Glide.with(holder.itemView.context)
.load(Uri.parse(result.videoUri))
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(holder.videoThumbnail)
android.util.Log.d("VideoAnalysisAdapter", "Loading video thumbnail for URI: ${result.videoUri}")
holder.videoThumbnail.setOnClickListener {
try {
val videoFile = File(Uri.parse(result.videoUri).path)
val context = holder.itemView.context
val contentUri: Uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
videoFile
)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(contentUri, "video/*")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(intent)
} catch (e: Exception) {
android.util.Log.e("VideoAnalysisAdapter", "无法播放视频: ${result.videoUri}, 错误: ${e.message}", e)
Toast.makeText(holder.itemView.context, "无法播放视频,请检查文件或权限", Toast.LENGTH_SHORT).show()
}
}
}
override fun getItemCount(): Int = results.size
}

@ -1,30 +0,0 @@
package org.tensorflow.lite.examples.poseestimation
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class VideoAnalysisResultActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_analysis_result)
val tvExerciseName = findViewById<TextView>(R.id.tv_result_exercise_name)
val tvScore = findViewById<TextView>(R.id.tv_result_score)
val tvEvaluation = findViewById<TextView>(R.id.tv_result_evaluation)
// 从 Intent 中获取数据
val exerciseName = intent.getStringExtra("exercise_name")
val score = intent.getFloatExtra("score", 0f)
val evaluation = intent.getStringExtra("evaluation")
// 设置数据显示
tvExerciseName.text = exerciseName ?: "未知动作"
tvScore.text = String.format("%.1f", score) // 格式化得分,保留一位小数
tvEvaluation.text = evaluation ?: "暂无评价"
// 设置标题 (可选)
supportActionBar?.title = "分析结果"
}
}

@ -20,7 +20,6 @@ class WeightSelectionActivity : AppCompatActivity() {
private var selectedGender: String? = null
private var selectedAge: Int = 0
private var username: String? = null
private var currentWeight = 54
private var lastY: Float = 0f
@ -35,7 +34,6 @@ class WeightSelectionActivity : AppCompatActivity() {
// 获取从上一个页面传递的数据
selectedGender = intent.getStringExtra("selected_gender")
selectedAge = intent.getIntExtra("selected_age", 0)
username = intent.getStringExtra("username")
// 初始化视图
selectedWeightText = findViewById(R.id.selectedWeightText)
@ -89,7 +87,6 @@ class WeightSelectionActivity : AppCompatActivity() {
intent.putExtra("selected_gender", selectedGender)
intent.putExtra("selected_age", selectedAge)
intent.putExtra("selected_weight", currentWeight)
intent.putExtra("username", username)
startActivity(intent)
finish()
}
@ -97,7 +94,6 @@ class WeightSelectionActivity : AppCompatActivity() {
backButton.setOnClickListener {
val intent = Intent(this, AgeSelectionActivity::class.java)
intent.putExtra("selected_gender", selectedGender)
intent.putExtra("username", username)
startActivity(intent)
finish()
}

@ -0,0 +1,49 @@
package org.tensorflow.lite.examples.poseestimation.auth;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DatabaseHelper extends SQLiteOpenHelper{
// 数据库信息
private static final String DATABASE_NAME = "user_database";
private static final int DATABASE_VERSION = 1;
// 表名和列名
public static final String TABLE_USERS = "users";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_USERNAME = "username";
public static final String COLUMN_PASSWORD_HASH = "password_hash";
public static final String COLUMN_AGE = "age";
public static final String COLUMN_WEIGHT = "weight";
public static final String COLUMN_GENDER = "gender";
public static final String COLUMN_HEIGHT = "height";
// 创建表的SQL语句
private static final String CREATE_TABLE_USERS =
"CREATE TABLE " + TABLE_USERS + "("
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ COLUMN_USERNAME + " TEXT UNIQUE NOT NULL,"
+ COLUMN_PASSWORD_HASH + " TEXT NOT NULL"
// + COLUMN_AGE + " INTEGER,"
// + COLUMN_WEIGHT + " REAL,"
// + COLUMN_GENDER + " TEXT,"
// + COLUMN_HEIGHT + " REAL"
+ ")";
// 构造函数
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_USERS);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_USERS);
onCreate(db);
}
}

@ -0,0 +1,46 @@
package org.tensorflow.lite.examples.poseestimation.auth;
import java.io.Serializable;
public class User implements Serializable{
private long id;
private String username;
private String passwordHash;
// private int age;
// private float weight;
// private String gender;
// private float height;
// 构造函数、Getter和Setter方法
public User() {}
public User(String username, String passwordHash) {
this.username = username;
this.passwordHash = passwordHash;
// this.age = age;
// this.weight = weight;
// this.gender = gender;
// this.height = height;
}
// Getter和Setter方法
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
// public int getAge() { return age; }
// public void setAge(int age) { this.age = age; }
//
// public float getWeight() { return weight; }
// public void setWeight(float weight) { this.weight = weight; }
//
// public String getGender() { return gender; }
// public void setGender(String gender) { this.gender = gender; }
//
// public float getHeight() { return height; }
// public void setHeight(float height) { this.height = height; }
}

@ -0,0 +1,130 @@
package org.tensorflow.lite.examples.poseestimation.auth;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import org.tensorflow.lite.examples.poseestimation.auth.User;
import java.util.ArrayList;
import java.util.List;
public class UserDao {
private DatabaseHelper databaseHelper;
public UserDao(Context context) {
databaseHelper = new DatabaseHelper(context);
}
// 添加用户
public long addUser(User user) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(DatabaseHelper.COLUMN_USERNAME, user.getUsername());
values.put(DatabaseHelper.COLUMN_PASSWORD_HASH, user.getPasswordHash());
// values.put(DatabaseHelper.COLUMN_AGE, user.getAge());
// values.put(DatabaseHelper.COLUMN_WEIGHT, user.getWeight());
// values.put(DatabaseHelper.COLUMN_GENDER, user.getGender());
// values.put(DatabaseHelper.COLUMN_HEIGHT, user.getHeight());
long id = db.insert(DatabaseHelper.TABLE_USERS, null, values);
db.close();
return id;
}
// 根据用户名获取用户
public User getUserByUsername(String username) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String[] projection = {
DatabaseHelper.COLUMN_ID,
DatabaseHelper.COLUMN_USERNAME,
DatabaseHelper.COLUMN_PASSWORD_HASH,
// DatabaseHelper.COLUMN_AGE,
// DatabaseHelper.COLUMN_WEIGHT,
// DatabaseHelper.COLUMN_GENDER,
// DatabaseHelper.COLUMN_HEIGHT
};
String selection = DatabaseHelper.COLUMN_USERNAME + " = ?";
String[] selectionArgs = { username };
Cursor cursor = db.query(
DatabaseHelper.TABLE_USERS,
projection,
selection,
selectionArgs,
null,
null,
null
);
User user = null;
if (cursor.moveToFirst()) {
user = new User();
user.setId(cursor.getLong(cursor.getColumnIndex(DatabaseHelper.COLUMN_ID)));
user.setUsername(cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_USERNAME)));
user.setPasswordHash(cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_PASSWORD_HASH)));
// user.setAge(cursor.getInt(cursor.getColumnIndex(DatabaseHelper.COLUMN_AGE)));
// user.setWeight(cursor.getFloat(cursor.getColumnIndex(DatabaseHelper.COLUMN_WEIGHT)));
// user.setGender(cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_GENDER)));
// user.setHeight(cursor.getFloat(cursor.getColumnIndex(DatabaseHelper.COLUMN_HEIGHT)));
}
cursor.close();
db.close();
return user;
}
// 检查用户名是否存在
public boolean checkUsernameExists(String username) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String[] projection = { DatabaseHelper.COLUMN_USERNAME };
String selection = DatabaseHelper.COLUMN_USERNAME + " = ?";
String[] selectionArgs = { username };
Cursor cursor = db.query(
DatabaseHelper.TABLE_USERS,
projection,
selection,
selectionArgs,
null,
null,
null
);
boolean exists = cursor.moveToFirst();
cursor.close();
db.close();
return exists;
}
// 获取所有用户
public List<User> getAllUsers() {
List<User> userList = new ArrayList<>();
String selectQuery = "SELECT * FROM " + DatabaseHelper.TABLE_USERS;
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.rawQuery(selectQuery, null);
if (cursor.moveToFirst()) {
do {
User user = new User();
user.setId(cursor.getLong(cursor.getColumnIndex(DatabaseHelper.COLUMN_ID)));
user.setUsername(cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_USERNAME)));
user.setPasswordHash(cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_PASSWORD_HASH)));
// user.setAge(cursor.getInt(cursor.getColumnIndex(DatabaseHelper.COLUMN_AGE)));
// user.setWeight(cursor.getFloat(cursor.getColumnIndex(DatabaseHelper.COLUMN_WEIGHT)));
// user.setGender(cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_GENDER)));
// user.setHeight(cursor.getFloat(cursor.getColumnIndex(DatabaseHelper.COLUMN_HEIGHT)));
userList.add(user);
} while (cursor.moveToNext());
}
cursor.close();
db.close();
return userList;
}
}

@ -264,7 +264,7 @@ class CameraSource(
// if the model returns only one item, show that item's score.
if (persons.isNotEmpty()) {
listener?.onDetectedInfo(persons[0].score, classificationResult, persons)
listener?.onDetectedInfo(persons[0].score, classificationResult)
}
visualize(persons, bitmap)
}
@ -322,10 +322,6 @@ class CameraSource(
interface CameraSourceListener {
fun onFPSListener(fps: Int)
fun onDetectedInfo(
personScore: Float?,
poseLabels: List<Pair<String, Float>>?,
persons: List<Person>
)
fun onDetectedInfo(personScore: Float?, poseLabels: List<Pair<String, Float>>?)
}
}

@ -1,48 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
@Database(entities = [User::class, UserProfile::class, VideoAnalysisResult::class], version = 6, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun userProfileDao(): UserProfileDao
abstract fun videoAnalysisResultDao(): VideoAnalysisResultDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
private val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `video_analysis_results` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `username` TEXT NOT NULL, `exerciseType` TEXT NOT NULL, `videoUri` TEXT NOT NULL, `score` REAL NOT NULL, `evaluation` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)"
)
}
}
private val MIGRATION_5_6 = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE user_profiles ADD COLUMN avatarUri TEXT")
}
}
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_4_5, MIGRATION_5_6)
.build()
INSTANCE = instance
instance
}
}
}
}

@ -1,13 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "users")
data class User(
@PrimaryKey
val username: String,
val password: String,
val signature: String? = null,
val avatarUri: String? = null
)

@ -1,27 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface UserDao {
@Insert
fun insertUser(user: User)
@Query("SELECT * FROM users WHERE username = :username AND password = :password")
fun getUser(username: String, password: String): Flow<User?>
@Query("SELECT * FROM users WHERE username = :username")
fun getUserByUsername(username: String): Flow<User?>
@Query("UPDATE users SET signature = :signature WHERE username = :username")
fun updateSignature(username: String, signature: String)
@Query("SELECT signature FROM users WHERE username = :username")
fun getSignature(username: String): Flow<String?>
@Query("UPDATE users SET avatarUri = :avatarUri WHERE username = :username")
fun updateAvatarUri(username: String, avatarUri: String?)
}

@ -1,14 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "user_profiles")
data class UserProfile(
@PrimaryKey val username: String, // 与User表关联
val gender: String,
val age: Int,
val weight: Int,
val height: Int,
val avatarUri: String? = null // 新增用户头像URI
)

@ -1,19 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface UserProfileDao {
@Insert
fun insertUserProfile(profile: UserProfile): Long
@Update
fun updateUserProfile(profile: UserProfile): Int
@Query("SELECT * FROM user_profiles WHERE username = :username")
fun getUserProfileByUsername(username: String): Flow<UserProfile?>
}

@ -1,15 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "video_analysis_results")
data class VideoAnalysisResult(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val username: String,
val exerciseType: String,
val videoUri: String,
val score: Float, // Simplified score for now
val evaluation: String,
val timestamp: Long // Timestamp for when the analysis was performed
)

@ -1,19 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface VideoAnalysisResultDao {
@Insert
fun insertVideoAnalysisResult(result: VideoAnalysisResult): Long
@Query("SELECT * FROM video_analysis_results WHERE username = :username ORDER BY timestamp DESC")
fun getVideoAnalysisResultsByUsername(username: String): Flow<List<VideoAnalysisResult>>
// Optional: Get results for a specific exercise type
@Query("SELECT * FROM video_analysis_results WHERE username = :username AND exerciseType = :exerciseType ORDER BY timestamp DESC")
fun getVideoAnalysisResultsByUsernameAndExerciseType(username: String, exerciseType: String): Flow<List<VideoAnalysisResult>>
}

@ -1,179 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.data
import android.graphics.PointF
import kotlin.math.abs
import kotlin.math.acos
import kotlin.math.atan2
import kotlin.math.sqrt
class Angle {
// 站立姿势(正面)
fun standList(): List<Int> {
val list = List(8) { 90; 90; 170; 170; 90; 90; 180; 180 }
return list
}
// 硬拉(左侧面)
fun deadliftList(): List<Int> {
val list = List(8) { 90; 90; 180; 180; 150; 30; 120; 120 }
return list
}
// 深蹲(侧面观察)
fun squatList(): List<Int> {
val list = List(8) { 10; 10; 180; 180; 20; 20; 70; 70 }
/*左肩膀与右肩膀保持水平
list[1] = 180 // 右肩膀与左肩膀保持水平
list[2] = 90 // 左肘部默认弯曲角度为90°
list[3] = 90 // 右肘部默认弯曲角度为90°
list[4] = 90 // 左髋部下降,接近平行
list[5] = 90 // 右髋部下降,接近平行
list[6] = 90 // 左膝盖弯曲至90°
list[7] = 90 // 右膝盖弯曲至90°*/
return list
}
// 平板支撑Plank- 侧面观察
fun plankList(): List<Int> {
val list = List(8) { 90; 90; 90; 90; 180; 180; 180; 180 }
/*list[0] = 180 // 左肩膀与右肩膀保持水平
list[1] = 180 // 右肩膀与左肩膀保持水平
list[2] = 0 // 左肘部保持水平
list[3] = 0 // 右肘部保持水平
list[4] = 0 // 左膝盖保持直线
list[5] = 0 // 右膝盖保持直线
list[6] = 0 // 左脚踝与脚背成直线
list[7] = 0 // 右脚踝与脚背成直线*/
return list
}
// 引体向上(正面)
fun pullUpList(): List<Int> {
val list = List(8) { 100; 100; 100; 100; 90; 90; 180; 180 }
return list
}
/*
俯卧撑Push-up- 侧面观察
肩膀LEFT_SHOULDER, RIGHT_SHOULDER肩膀应在肘部正上方保持稳定不要耸肩 -> 左右肩膀的角度应接近180°保持在肘部上方
肘部LEFT_ELBOW, RIGHT_ELBOW肘部弯曲角度大约为90度且肘部始终保持在肩膀下方 -> 左右肘部角度为90°肘部应下沉不应过度弯曲
脚踝LEFT_ANKLE, RIGHT_ANKLE身体应从脚踝到肩膀成一直线避免臀部上升或下垂 -> 左右脚踝角度为0°保持直线
*/
fun pushUpList(): List<Int> {
val list = List(8) { 90 } // 初始值设置为90°
/*list[0] = 180 // 左肩膀与右肩膀保持水平
list[1] = 180 // 右肩膀与左肩膀保持水平
list[2] = 90 // 左肘部弯曲至90°
list[3] = 90 // 右肘部弯曲至90°
list[4] = 0 // 左膝盖保持直线
list[5] = 0 // 右膝盖保持直线
list[6] = 0 // 左脚踝角度保持自然
list[7] = 0 // 右脚踝角度保持自然*/
return list
}
}
class AngleDifference(private val angleObj: Angle) {
// 根据关键点计算关节角度
fun getAngles(keyPoints: List<KeyPoint>): List<Double> {
val angles = mutableListOf<Double>()
// 定义需要计算角度的关节及其组成关键点
// 顺序对应 BodyParts: 左肩, 右肩, 左肘, 右肘, 左髋, 右髋, 左膝, 右膝
val angleJoints = listOf(
Triple(BodyPart.LEFT_ELBOW, BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER), // 左肩角度
Triple(BodyPart.RIGHT_ELBOW, BodyPart.RIGHT_SHOULDER, BodyPart.LEFT_SHOULDER), // 右肩角度
Triple(BodyPart.LEFT_SHOULDER, BodyPart.LEFT_ELBOW, BodyPart.LEFT_WRIST), // 左肘角度
Triple(BodyPart.RIGHT_SHOULDER, BodyPart.RIGHT_ELBOW, BodyPart.RIGHT_WRIST), // 右肘角度
Triple(BodyPart.LEFT_SHOULDER, BodyPart.LEFT_HIP, BodyPart.RIGHT_HIP), // 左髋角度
Triple(BodyPart.RIGHT_SHOULDER, BodyPart.RIGHT_HIP, BodyPart.LEFT_HIP), // 右髋角度
Triple(BodyPart.LEFT_HIP, BodyPart.LEFT_KNEE, BodyPart.LEFT_ANKLE), // 左膝角度
Triple(BodyPart.RIGHT_HIP, BodyPart.RIGHT_KNEE, BodyPart.RIGHT_ANKLE) // 右膝角度
)
for (joint in angleJoints) {
val p1 = keyPoints.find { it.bodyPart == joint.first }
val p2 = keyPoints.find { it.bodyPart == joint.second }
val p3 = keyPoints.find { it.bodyPart == joint.third }
// 确保三个关键点都检测到且置信度较高
if (p1 != null && p2 != null && p3 != null && p1.score > 0.2 && p2.score > 0.2 && p3.score > 0.2) { // 假设置信度阈值为0.2
angles.add(p2.abttPoints(p1, p2, p3).toDouble())
} else {
angles.add(0.0) // 如果关键点未检测到或置信度低角度设为0或NaN具体取决于你如何处理无效数据
}
}
return angles
}
// 根据不同的 listType 判断标准
private fun getStandardForListType(listType: String): List<Double> {
return when (listType) {
"1" -> angleObj.standList().map { it.toDouble() } // 转换为 List<Double>
"2" -> angleObj.deadliftList().map { it.toDouble() } // 转换为 List<Double>
"3" -> angleObj.pushUpList().map { it.toDouble() } // 转换为 List<Double>
"4" -> angleObj.squatList().map { it.toDouble() } // 转换为 List<Double>
"5" -> angleObj.plankList().map { it.toDouble() } // 转换为 List<Double>
else -> throw IllegalArgumentException("未知的列表类型: $listType") // 无效的输入
}
}
// 计算角度差并根据角度值判断是否大于10返回相应的语句
fun calculateAngleDifference(keyPoints: List<KeyPoint>, listType: String): String {
// 根据传入的参数选择不同的角度列表 (标准角度)
val standardList = getStandardForListType(listType)
// 获取从关键点计算出的角度列表
val angles = getAngles(keyPoints)
// 确保两个列表的大小一致
if (standardList.size != angles.size) {
// throw IllegalArgumentException("标准列表和计算出的角度列表大小不一致!")
// 如果大小不一致,可能是关键点没有全部检测到,返回一个提示信息而不是崩溃
return "关键点检测不完整,无法给出建议。"
}
// 计算差值并检查条件
val differenceList = mutableListOf<Double>()
for (i in standardList.indices) {
differenceList.add((standardList[i] - angles[i]))
}
// 根据listType的不同提供不同标准的判断
val absoluteValues = differenceList.map { abs(it) }
// 检查角度差是否符合标准
val warningMessages = mutableListOf<String>()
// 替换角度位置的名称
val bodyParts = listOf(
"左肩", "右肩", "左肘", "右肘", "左髋", "右髋", "左膝", "右膝"
)
for (i in absoluteValues.indices) {
val standard = standardList[i]
val calculatedAngle = angles[i]
val absoluteDifference = absoluteValues[i]
// 仅对检测到且置信度较高的关键点进行角度判断
// 这里假设 getAngles 中已经处理了置信度低的,如果 getAngles 返回0.0表示无效,则跳过
if (calculatedAngle != 0.0 && absoluteDifference > 10) { // 角度差大于10度给出建议
// 根据角度差的正负判断提示语
val direction = if (differenceList[i] > 0) "增大" else "减小"
warningMessages.add("${bodyParts[i]}角度(${String.format("%.1f", calculatedAngle)}°)与标准(${standard}°)相差过大,请注意调整!")
}
}
// 如果没有超出标准的位置
return if (warningMessages.isEmpty()) {
"动作标准"
} else {
warningMessages.joinToString("\n")
}
}
}

@ -1,239 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.evaluator
import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
import org.tensorflow.lite.examples.poseestimation.data.BodyPart
import kotlin.math.abs
import kotlin.math.roundToInt
// 定义硬拉动作的阶段
enum class DeadliftState {
START, // 起始姿态
DESCENT, // 下降过程
BOTTOM, // 底部姿态
ASCENT, // 上升过程
LOCKOUT // 锁定(完成)姿态
}
data class EvaluationMessage(
val message: String,
val isError: Boolean = false
)
class DeadliftEvaluator : ExerciseEvaluator {
private var currentState: DeadliftState = DeadliftState.START
private var totalScore: Float = 0f
private var frameCount: Int = 0
private var evaluationMessages: MutableList<EvaluationMessage> = mutableListOf()
private var repCount: Int = 0
private var isRepCounting: Boolean = false
// 硬拉关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
private val HIP_ANGLE_START_MAX = 170f // 站立时髋关节角度
private val KNEE_ANGLE_START_MAX = 170f // 站立时膝关节角度
private val TORSO_ANGLE_START_MAX = 10f // 站立时躯干与垂直方向最大夹角
private val HIP_ANGLE_BOTTOM_MIN = 70f // 硬拉底部髋关节最小角度
private val KNEE_ANGLE_BOTTOM_MIN = 90f // 硬拉底部膝关节最小角度
private val TORSO_ANGLE_BOTTOM_MAX = 45f // 硬拉底部躯干与垂直方向最大夹角
private val BACK_STRAIGHT_THRESHOLD = 160f // 躯干挺直角度阈值 (肩-髋-膝角度)
// Helper function to calculate angle between three keypoints (A-B-C)
// B is the vertex of the angle
private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
// Use the abttPoints function from KeyPoint.kt
return B.abttPoints(A, B, C)
}
override fun evaluateFrame(keyPoints: List<KeyPoint>) {
if (keyPoints.isEmpty()) {
evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
return
}
frameCount++
// 获取硬拉所需关键点
val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
val leftKnee = keyPoints.find { it.bodyPart == BodyPart.LEFT_KNEE }
val rightKnee = keyPoints.find { it.bodyPart == BodyPart.RIGHT_KNEE }
val leftAnkle = keyPoints.find { it.bodyPart == BodyPart.LEFT_ANKLE }
val rightAnkle = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ANKLE }
// 确保所有关键点都存在,如果缺失,则跳过此帧或给出警告
if (leftShoulder == null || rightShoulder == null || leftHip == null || rightHip == null ||
leftKnee == null || rightKnee == null || leftAnkle == null || rightAnkle == null) {
evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
return
}
// 计算左右平均关键点,提高稳定性
val midShoulder = KeyPoint(
BodyPart.NOSE, // 使用 NOSE 作为占位符,因为没有一个"中肩"的 BodyPart
android.graphics.PointF(
(leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
(leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
),
(leftShoulder.score + rightShoulder.score) / 2
)
val midHip = KeyPoint(
BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
android.graphics.PointF(
(leftHip.coordinate.x + rightHip.coordinate.x) / 2,
(leftHip.coordinate.y + rightHip.coordinate.y) / 2
),
(leftHip.score + rightHip.score) / 2
)
val midKnee = KeyPoint(
BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
android.graphics.PointF(
(leftKnee.coordinate.x + rightKnee.coordinate.x) / 2,
(leftKnee.coordinate.y + rightKnee.coordinate.y) / 2
),
(leftKnee.score + rightKnee.score) / 2
)
val midAnkle = KeyPoint(
BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
android.graphics.PointF(
(leftAnkle.coordinate.x + rightAnkle.coordinate.x) / 2,
(leftAnkle.coordinate.y + rightAnkle.coordinate.y) / 2
),
(leftAnkle.score + rightAnkle.score) / 2
)
// 计算核心角度
val hipAngle = calculateAngle(midShoulder, midHip, midKnee) // 肩-髋-膝
val kneeAngle = calculateAngle(midHip, midKnee, midAnkle) // 髋-膝-踝
// 躯干角度 (这里简化为肩与髋的y坐标差与x坐标差的反正切并转换为相对于垂直线的角度)
// 更准确的躯干角度需要更复杂的向量计算,例如通过肩-髋连线与竖直方向的夹角
val torsoAngle = abs(midShoulder.abtPoints(midHip).second - 90f) // 假设 midShoulder.abtPoints(midHip).second 是与Y轴的夹角减去90得到与X轴的夹角再取绝对值但这里我们想要与垂直线的夹角所以需要调整
// 简化的躯干角度直接使用肩和髋的y坐标差与x坐标差的反正切
val dxTorso = midHip.coordinate.x - midShoulder.coordinate.x
val dyTorso = midHip.coordinate.y - midShoulder.coordinate.y
val torsoAngleVertical = Math.toDegrees(kotlin.math.atan2(dxTorso.toDouble(), dyTorso.toDouble())).toFloat() // 与y轴正方向的夹角
// 如果要与垂直方向的夹角,通常是 90 - atan2(dy, dx)
// 或者直接根据象限判断
val realTorsoAngle = abs(torsoAngleVertical) // 简化处理,取绝对值
var frameScore = 0f
var frameEvaluation = ""
when (currentState) {
DeadliftState.START -> {
// 评估起始姿态
if (hipAngle > HIP_ANGLE_START_MAX && kneeAngle > KNEE_ANGLE_START_MAX && realTorsoAngle < TORSO_ANGLE_START_MAX) {
frameEvaluation = "很棒的起始姿态!准备好向下,让身体进入训练模式。"
isRepCounting = false // 重置,等待下一次下降
currentState = DeadliftState.DESCENT
frameScore = 300f
} else {
frameEvaluation = "小提示:起始姿态可以再调整一下。试着站得更直,核心收得更紧,为硬拉做好准备!"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
}
DeadliftState.DESCENT -> {
// 评估下降过程
if (hipAngle < HIP_ANGLE_START_MAX && kneeAngle < KNEE_ANGLE_START_MAX) { // 髋和膝盖开始弯曲
if (realTorsoAngle < BACK_STRAIGHT_THRESHOLD) { // 检查背部是否挺直
frameEvaluation = "下降得很稳!记得保持背部挺直,感受髋部的发力。"
currentState = DeadliftState.BOTTOM
frameScore = 300f
} else {
frameEvaluation = "下降时背部有点弓起哦!试着挺胸收腹,保持脊柱中立,会更安全有效。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
} else {
frameEvaluation = "继续向下,保持动作流畅,感受肌肉的拉伸。"
}
}
DeadliftState.BOTTOM -> {
// 评估底部姿态
if (hipAngle >= HIP_ANGLE_BOTTOM_MIN && hipAngle <= HIP_ANGLE_START_MAX &&
kneeAngle >= KNEE_ANGLE_BOTTOM_MIN && kneeAngle <= KNEE_ANGLE_START_MAX &&
realTorsoAngle <= TORSO_ANGLE_BOTTOM_MAX) {
frameEvaluation = "底部姿态非常稳定!现在准备向上发力,让身体爆发起来!"
currentState = DeadliftState.ASCENT
frameScore = 450f
} else {
frameEvaluation = "底部姿态可以再优化一下。尝试让髋部再低一些,膝盖不要超过脚尖太多,同时保持背部挺直。你离完美不远了!"
if (hipAngle < HIP_ANGLE_BOTTOM_MIN) evaluationMessages.add(EvaluationMessage("髋部下降不足。", true))
if (kneeAngle < KNEE_ANGLE_BOTTOM_MIN) evaluationMessages.add(EvaluationMessage("膝盖弯曲不足。", true))
if (realTorsoAngle > TORSO_ANGLE_BOTTOM_MAX) evaluationMessages.add(EvaluationMessage("注意!你的背部有些弓起或者过度前倾了。记得整个过程都要保持背部挺直,这是硬拉的关键!", true))
}
}
DeadliftState.ASCENT -> {
// 评估上升过程
if (hipAngle > HIP_ANGLE_BOTTOM_MIN && kneeAngle > KNEE_ANGLE_BOTTOM_MIN) { // 髋和膝盖开始伸展
if (realTorsoAngle < BACK_STRAIGHT_THRESHOLD) { // 检查背部是否挺直
frameEvaluation = "向上发力非常有力!继续保持背部挺直,让髋部和膝盖同步伸展,爆发力十足!"
currentState = DeadliftState.LOCKOUT
frameScore = 300f
} else {
frameEvaluation = "上升时背部有些弓起了。集中注意力,保持背部挺直,用臀部发力带动身体向上。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
} else {
frameEvaluation = "加油,继续向上,把身体完全推回起始位置!"
}
}
DeadliftState.LOCKOUT -> {
// 评估锁定姿态
if (hipAngle > HIP_ANGLE_START_MAX - 5 && kneeAngle > KNEE_ANGLE_START_MAX - 5 && realTorsoAngle < TORSO_ANGLE_START_MAX + 5) {
frameEvaluation = "完美完成一次!锁定姿态非常棒,全身收紧,力量感十足!"
if (!isRepCounting) {
repCount++
isRepCounting = true
evaluationMessages.add(EvaluationMessage("恭喜你,又完成了一次硬拉!目前累计完成了 $repCount 次,继续保持!"))
}
currentState = DeadliftState.START // 完成一次后回到起始状态,准备下一次
frameScore = 150f
} else {
frameEvaluation = "快完成了!在顶部时,记得让髋部和膝盖完全伸展,轻轻收紧臀部,肩膀向后收拢,充分锁定。"
if (hipAngle < HIP_ANGLE_START_MAX - 5 || kneeAngle < KNEE_ANGLE_START_MAX - 5) evaluationMessages.add(EvaluationMessage("顶部锁定还不够充分哦,髋部和膝盖可以再伸展一点。", true))
if (realTorsoAngle > TORSO_ANGLE_START_MAX + 5) evaluationMessages.add(EvaluationMessage("注意了!在锁定阶段,身体可能有点过度后仰或者背部没有完全挺直。保持核心收紧,稳稳地完成动作。", true))
}
}
}
totalScore += frameScore // 累加每帧分数
evaluationMessages.add(EvaluationMessage(frameEvaluation)) // 添加每帧的评估
}
override fun getFinalScore(): Float {
val rawScore = if (frameCount > 0) totalScore / frameCount else 0f
val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
return roundedScore.coerceIn(0f, 100f)
}
override fun getFinalEvaluation(finalScore: Float): String {
val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
val overallEvaluationBuilder = StringBuilder()
if (errors.isEmpty()) {
when (finalScore.toInt()) {
in 90..100 -> overallEvaluationBuilder.append("太棒了!你的硬拉动作几乎完美无瑕,姿态标准,力量十足!继续保持!")
in 70..89 -> overallEvaluationBuilder.append("非常不错的硬拉!动作基本流畅,姿态也比较到位,再稍加注意细节就能更完美!")
in 50..69 -> overallEvaluationBuilder.append("硬拉动作有进步空间哦!虽然有些地方做得不错,但还需要多练习,让姿态更稳定、发力更集中。")
in 30..49 -> overallEvaluationBuilder.append("本次硬拉需要更多练习。动作中存在一些明显的姿态问题,这会影响训练效果和安全性。")
else -> overallEvaluationBuilder.append("硬拉动作仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
}
} else {
overallEvaluationBuilder.append("本次硬拉分析完成!发现了一些可以改进的地方:\n")
overallEvaluationBuilder.append(errors.joinToString("\n"))
}
overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
overallEvaluationBuilder.append(uniqueMessages)
return overallEvaluationBuilder.toString()
}
}

@ -1,9 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.evaluator
import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
interface ExerciseEvaluator {
fun evaluateFrame(keyPoints: List<KeyPoint>)
fun getFinalScore(): Float
fun getFinalEvaluation(finalScore: Float): String
}

@ -1,198 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.evaluator
import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
import org.tensorflow.lite.examples.poseestimation.data.BodyPart
import kotlin.math.abs
import kotlin.math.roundToInt
// 定义平板支撑动作的阶段
enum class PlankState {
HOLDING, // 保持姿态
ERROR // 错误姿态
}
class PlankEvaluator : ExerciseEvaluator {
private var currentState: PlankState = PlankState.HOLDING
private var totalScore: Float = 0f
private var frameCount: Int = 0
private var evaluationMessages: MutableList<EvaluationMessage> = mutableListOf()
private var timeInCorrectPoseMs: Long = 0L // 记录正确姿态的持续时间
private var lastFrameTimestamp: Long = 0L
// 平板支撑关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
private val HIP_SHOULDER_ANKLE_MIN = 160f // 髋-肩-踝角度,用于判断身体是否呈直线
private val HIP_SHOULDER_ANKLE_MAX = 185f // 允许略微的弧度
private val HIP_SAG_THRESHOLD_PERCENT = 0.05f // 臀部下沉阈值(相对于肩-踝距离的百分比)
private val HIP_PIKE_THRESHOLD_PERCENT = 0.05f // 臀部过高阈值
// Helper function to calculate angle between three keypoints (A-B-C)
// B is the vertex of the angle
private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
return B.abttPoints(A, B, C)
}
override fun evaluateFrame(keyPoints: List<KeyPoint>) {
if (keyPoints.isEmpty()) {
evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
return
}
frameCount++
val currentTimestamp = System.currentTimeMillis() // 假设每帧的时间间隔可以通过外部传入或自行估算
if (lastFrameTimestamp != 0L) {
val deltaTimeMs = currentTimestamp - lastFrameTimestamp
// 假设视频帧率稳定或者直接使用帧间隔例如100ms一帧
// 这里简化处理,直接使用一个固定的帧分数,而不是基于时间
}
lastFrameTimestamp = currentTimestamp
// 获取平板支撑所需关键点
val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
val leftAnkle = keyPoints.find { it.bodyPart == BodyPart.LEFT_ANKLE }
val rightAnkle = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ANKLE }
val nose = keyPoints.find { it.bodyPart == BodyPart.NOSE }
// 确保所有关键点都存在
if (leftShoulder == null || rightShoulder == null || leftHip == null || rightHip == null ||
leftAnkle == null || rightAnkle == null || nose == null) {
evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
return
}
// 计算左右平均关键点,提高稳定性
val midShoulder = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
(leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
),
(leftShoulder.score + rightShoulder.score) / 2
)
val midHip = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftHip.coordinate.x + rightHip.coordinate.x) / 2,
(leftHip.coordinate.y + rightHip.coordinate.y) / 2
),
(leftHip.score + rightHip.score) / 2
)
val midAnkle = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftAnkle.coordinate.x + rightAnkle.coordinate.x) / 2,
(leftAnkle.coordinate.y + rightAnkle.coordinate.y) / 2
),
(leftAnkle.score + rightAnkle.score) / 2
)
// 计算身体直线角度(肩-髋-踝)
val bodyLineAngle = calculateAngle(midShoulder, midHip, midAnkle)
// 计算头部姿态 (肩-鼻-肩 角度或直接看鼻子的Y坐标相对肩的Y坐标)
val headY = nose.coordinate.y
val shoulderY = midShoulder.coordinate.y
var headDrop = false
if (headY > shoulderY + 20) { // 假设头部Y坐标显著低于肩部Y坐标视为下垂
headDrop = true
}
var frameScore = 0f
var frameEvaluation = ""
var isCorrectPose = true
// 新增臀部位置检查
val ankleY = midAnkle.coordinate.y
val hipY = midHip.coordinate.y
// 计算肩部到踝部的垂直距离
val verticalDistanceShoulderAnkle = abs(shoulderY - ankleY)
val hipSagThreshold = verticalDistanceShoulderAnkle * HIP_SAG_THRESHOLD_PERCENT
val hipPikeThreshold = verticalDistanceShoulderAnkle * HIP_PIKE_THRESHOLD_PERCENT
// 假设理想的髋部Y坐标应该在肩部和踝部的Y坐标之间或者与其中一个大致平齐
// 更精确地我们假设身体呈直线时髋部Y坐标应该与肩部和踝部连线的Y坐标在同一直线上
// 我们可以计算肩部和踝部连线在髋部X坐标处的Y值
val expectedHipY = shoulderY + (ankleY - shoulderY) * ((midHip.coordinate.x - midShoulder.coordinate.x) / (midAnkle.coordinate.x - midShoulder.coordinate.x))
var hipSag = false
var hipPike = false
if (hipY > expectedHipY + hipSagThreshold) { // 臀部Y坐标低于预期表示下沉
hipSag = true
isCorrectPose = false
evaluationMessages.add(EvaluationMessage("臀部下沉了!收紧核心,将臀部向上抬起,保持身体的平直。", true))
} else if (hipY < expectedHipY - hipPikeThreshold) { // 臀部Y坐标高于预期表示过高
hipPike = true
isCorrectPose = false
evaluationMessages.add(EvaluationMessage("臀部抬得太高了!尝试放低臀部,让身体呈一条直线。", true))
}
// 评估身体是否呈直线 (现在只关注角度,垂直位置由上面新的逻辑处理)
if (bodyLineAngle !in HIP_SHOULDER_ANKLE_MIN..HIP_SHOULDER_ANKLE_MAX) {
isCorrectPose = false
evaluationMessages.add(EvaluationMessage("身体不够平直,请调整髋部位置。", true))
}
// 评估头部姿态
if (headDrop) {
isCorrectPose = false
evaluationMessages.add(EvaluationMessage("头部下垂,请保持颈部中立,眼睛看向地面。", true))
}
if (isCorrectPose) {
frameEvaluation = "姿态保持良好,身体呈一条直线。"
frameScore = 100f // 正确姿态每帧获得固定分数
timeInCorrectPoseMs += (currentTimestamp - lastFrameTimestamp) // 累加正确姿态时间
} else {
frameEvaluation = "请调整姿态,注意身体的直线。"
frameScore = 0f
currentState = PlankState.ERROR // 进入错误姿态状态
}
totalScore += frameScore
evaluationMessages.add(EvaluationMessage(frameEvaluation))
// 检查状态转换如果从错误姿态恢复到正确姿态则重新设置为HOLDING
if (currentState == PlankState.ERROR && isCorrectPose) {
currentState = PlankState.HOLDING
}
}
override fun getFinalScore(): Float {
// 平板支撑的最终分数更侧重于保持正确姿态的时长
// 这里简化为正确姿态帧数占总帧数的比例并四舍五入到10的倍数
val rawScore = if (frameCount > 0) (totalScore / frameCount) else 0f
val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
return roundedScore.coerceIn(0f, 100f)
}
override fun getFinalEvaluation(finalScore: Float): String {
val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
val overallEvaluationBuilder = StringBuilder()
if (errors.isEmpty()) {
when (finalScore.toInt()) {
in 90..100 -> overallEvaluationBuilder.append("太棒了!你的平板支撑姿态几乎完美无瑕,身体呈一条直线,核心非常稳定!")
in 70..89 -> overallEvaluationBuilder.append("非常不错的平板支撑!姿态基本保持良好,再稍加注意细节就能更完美!")
in 50..69 -> overallEvaluationBuilder.append("平板支撑姿态有进步空间哦!虽然有些地方做得不错,但还需要多练习,让身体更稳定、更平直。")
in 30..49 -> overallEvaluationBuilder.append("本次平板支撑需要更多练习。姿态中存在一些明显的姿态问题,这会影响训练效果。")
else -> overallEvaluationBuilder.append("平板支撑姿态仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
}
} else {
overallEvaluationBuilder.append("本次平板支撑分析完成!发现了一些可以改进的地方:\n")
overallEvaluationBuilder.append(errors.joinToString("\n"))
// 对于平板支撑,时间更重要,但目前没有直接累加,可以考虑后期优化
}
overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
overallEvaluationBuilder.append(uniqueMessages)
return overallEvaluationBuilder.toString()
}
}

@ -1,240 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.evaluator
import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
import org.tensorflow.lite.examples.poseestimation.data.BodyPart
import kotlin.math.abs
import kotlin.math.roundToInt
// 定义引体向上动作的阶段
enum class PullUpState {
START, // 起始姿态(完全下放)
ASCENT, // 上拉过程
TOP, // 顶部姿态(下巴过杠)
DESCENT, // 下放过程
LOCKOUT // 恢复到起始姿态
}
class PullUpEvaluator : ExerciseEvaluator {
private var currentState: PullUpState = PullUpState.START
private var totalScore: Float = 0f
private var frameCount: Int = 0
private var evaluationMessages: MutableList<EvaluationMessage> = mutableListOf()
private var repCount: Int = 0
private var isRepCounting: Boolean = false
// 引体向上关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
private val ELBOW_ANGLE_START_MIN = 170f // 起始姿态(手臂伸直,进一步放宽要求)
private val SHOULDER_ELBOW_WRIST_TOP_MAX = 80f // 顶部姿态(手肘弯曲程度,进一步放宽要求)
private val NOSE_BAR_HEIGHT_THRESHOLD = 0f // 鼻子与杠的相对高度(需要通过实际坐标判断)
private val BACK_STRAIGHT_THRESHOLD = 160f // 躯干挺直角度阈值 (肩-髋-膝角度,用于判断背部是否弓起)
// Helper function to calculate angle between three keypoints (A-B-C)
// B is the vertex of the angle
private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
return B.abttPoints(A, B, C)
}
override fun evaluateFrame(keyPoints: List<KeyPoint>) {
if (keyPoints.isEmpty()) {
evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
return
}
frameCount++
// 获取引体向上所需关键点
val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
val leftElbow = keyPoints.find { it.bodyPart == BodyPart.LEFT_ELBOW }
val rightElbow = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ELBOW }
val leftWrist = keyPoints.find { it.bodyPart == BodyPart.LEFT_WRIST }
val rightWrist = keyPoints.find { it.bodyPart == BodyPart.RIGHT_WRIST }
val nose = keyPoints.find { it.bodyPart == BodyPart.NOSE }
val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
val leftKnee = keyPoints.find { it.bodyPart == BodyPart.LEFT_KNEE }
val rightKnee = keyPoints.find { it.bodyPart == BodyPart.RIGHT_KNEE }
// 确保所有关键点都存在
if (leftShoulder == null || rightShoulder == null || leftElbow == null || rightElbow == null ||
leftWrist == null || rightWrist == null || nose == null || leftHip == null || rightHip == null ||
leftKnee == null || rightKnee == null) {
evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
return
}
// 计算左右平均关键点,提高稳定性
val midShoulder = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
(leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
),
(leftShoulder.score + rightShoulder.score) / 2
)
val midElbow = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftElbow.coordinate.x + rightElbow.coordinate.x) / 2,
(leftElbow.coordinate.y + rightElbow.coordinate.y) / 2
),
(leftElbow.score + rightElbow.score) / 2
)
val midWrist = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftWrist.coordinate.x + rightWrist.coordinate.x) / 2,
(leftWrist.coordinate.y + rightWrist.coordinate.y) / 2
),
(leftWrist.score + rightWrist.score) / 2
)
val midHip = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftHip.coordinate.x + rightHip.coordinate.x) / 2,
(leftHip.coordinate.y + rightHip.coordinate.y) / 2
),
(leftHip.score + rightHip.score) / 2
)
val midKnee = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftKnee.coordinate.x + rightKnee.coordinate.x) / 2,
(leftKnee.coordinate.y + rightKnee.coordinate.y) / 2
),
(leftKnee.score + rightKnee.score) / 2
)
// 计算核心角度
val elbowAngle = calculateAngle(midShoulder, midElbow, midWrist) // 肩-肘-腕
val torsoAngle = calculateAngle(midShoulder, midHip, midKnee) // 肩-髋-膝,用于评估背部姿态
// 假设杠的位置,这里需要根据实际情况调整,或者通过用户输入来设定
// 这里暂时用一个简化的逻辑假设杠在肩部上方一定距离通过鼻子的Y坐标与肩的Y坐标判断
val isChinOverBar = nose.coordinate.y < midWrist.coordinate.y + 0 // 鼻子的Y坐标不低于手腕Y坐标大幅放宽
var frameScore = 0f
var frameEvaluation = ""
when (currentState) {
PullUpState.START -> {
// 评估起始姿态 (手臂完全伸直)
if (elbowAngle > ELBOW_ANGLE_START_MIN - 15) { // 进一步放宽手臂伸直要求
frameEvaluation = "起始姿态良好,手臂基本伸直,准备上拉。"
isRepCounting = false
currentState = PullUpState.ASCENT
frameScore = 250f // 适当提高分数
} else {
frameEvaluation = "请基本伸直手臂,回到起始位置。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
}
PullUpState.ASCENT -> {
// 评估上拉过程
if (elbowAngle < ELBOW_ANGLE_START_MIN - 10) { // 手肘开始弯曲,与起始角度保持衔接
if (torsoAngle > BACK_STRAIGHT_THRESHOLD) { // 检查背部是否挺直
if (isChinOverBar) { // 下巴过杠
frameEvaluation = "上拉有力,下巴已过杠,非常棒!"
currentState = PullUpState.TOP
frameScore = 450f // 适当提高分数
} else {
frameEvaluation = "继续上拉,努力让下巴过杠。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
} else {
frameEvaluation = "上拉时背部有点弓起。请保持背部挺直。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
} else {
frameEvaluation = "继续上拉,感受背部和手臂的发力。"
}
}
PullUpState.TOP -> {
// 评估顶部姿态 (下巴过杠,手肘弯曲)
if (isChinOverBar && elbowAngle < SHOULDER_ELBOW_WRIST_TOP_MAX + 30 && torsoAngle > BACK_STRAIGHT_THRESHOLD) { // 进一步放宽手肘弯曲和躯干角度
frameEvaluation = "完美!下巴过杠,顶部姿态保持得很好。"
currentState = PullUpState.DESCENT
frameScore = 700f // 适当提高分数
} else {
if (!isChinOverBar) evaluationMessages.add(EvaluationMessage("顶部锁定不充分,下巴未过杠。", true))
if (elbowAngle >= SHOULDER_ELBOW_WRIST_TOP_MAX + 30) evaluationMessages.add(EvaluationMessage("手肘弯曲不足,请再用力拉高。", true))
if (torsoAngle <= BACK_STRAIGHT_THRESHOLD) evaluationMessages.add(EvaluationMessage("顶部姿态背部弓起。", true))
frameEvaluation = "顶部姿态可以保持更稳定,确保下巴完全过杠。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
}
PullUpState.DESCENT -> {
// 评估下放过程
if (elbowAngle > SHOULDER_ELBOW_WRIST_TOP_MAX + 10) { // 手肘开始伸直,与顶部角度保持衔接
if (torsoAngle > BACK_STRAIGHT_THRESHOLD) { // 检查背部是否挺直
if (elbowAngle > ELBOW_ANGLE_START_MIN - 20) { // 进一步放宽回到起始位置的要求
frameEvaluation = "下放得很稳,即将完成一次引体向上。"
currentState = PullUpState.LOCKOUT
frameScore = 450f // 适当提高分数
} else {
frameEvaluation = "继续缓慢下放,控制好身体。"
}
} else {
frameEvaluation = "下放时背部有点弓起。请保持背部挺直。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
} else {
frameEvaluation = "继续下放,感受背部肌肉的拉伸。"
}
}
PullUpState.LOCKOUT -> {
// 评估完全下放(锁定)姿态
if (elbowAngle > ELBOW_ANGLE_START_MIN - 20 && torsoAngle > BACK_STRAIGHT_THRESHOLD) { // 进一步放宽手臂伸直要求
frameEvaluation = "完美完成一次引体向上!手臂基本伸直,为下一次做准备。"
if (!isRepCounting) {
repCount++
isRepCounting = true
evaluationMessages.add(EvaluationMessage("恭喜你,又完成了一次引体向上!目前累计完成了 $repCount 次,继续保持!"))
}
currentState = PullUpState.START // 完成一次后回到起始状态,准备下一次
frameScore = 250f // 适当提高分数
} else {
if (elbowAngle < ELBOW_ANGLE_START_MIN - 20) evaluationMessages.add(EvaluationMessage("手臂未完全伸直,请确保回到起始位置。", true))
if (torsoAngle <= BACK_STRAIGHT_THRESHOLD) evaluationMessages.add(EvaluationMessage("锁定姿态背部弓起。", true))
frameEvaluation = "请完成锁定:手臂基本伸直,身体保持平直。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
}
}
totalScore += frameScore
evaluationMessages.add(EvaluationMessage(frameEvaluation))
}
override fun getFinalScore(): Float {
val rawScore = if (frameCount > 0) totalScore / frameCount else 0f
val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
return roundedScore.coerceIn(0f, 100f)
}
override fun getFinalEvaluation(finalScore: Float): String {
val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
val overallEvaluationBuilder = StringBuilder()
if (errors.isEmpty()) {
when (finalScore.toInt()) {
in 90..100 -> overallEvaluationBuilder.append("太棒了!你的引体向上动作几乎完美无瑕,姿态标准,力量十足!继续保持!")
in 70..89 -> overallEvaluationBuilder.append("非常不错的引体向上!动作基本流畅,姿态也比较到位,再稍加注意细节就能更完美!")
in 50..69 -> overallEvaluationBuilder.append("引体向上动作有进步空间哦!虽然有些地方做得不错,但还需要多练习,让姿态更稳定、发力更集中。")
in 30..49 -> overallEvaluationBuilder.append("本次引体向上需要更多练习。动作中存在一些明显的姿态问题,这会影响训练效果和安全性。")
else -> overallEvaluationBuilder.append("引体向上动作仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
}
} else {
overallEvaluationBuilder.append("本次引体向上分析完成!发现了一些可以改进的地方:\n")
overallEvaluationBuilder.append(errors.joinToString("\n"))
overallEvaluationBuilder.append("\n总次数:$repCount")
}
overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
overallEvaluationBuilder.append(uniqueMessages)
return overallEvaluationBuilder.toString()
}
}

@ -1,237 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.evaluator
import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
import org.tensorflow.lite.examples.poseestimation.data.BodyPart
import kotlin.math.abs
import kotlin.math.roundToInt
// 定义俯卧撑动作的阶段
enum class PushUpState {
START, // 起始姿态(手臂伸直)
DESCENT, // 下降过程
BOTTOM, // 底部姿态(胸部接近地面)
ASCENT, // 上推过程
LOCKOUT // 恢复到起始姿态
}
class PushUpEvaluator : ExerciseEvaluator {
private var currentState: PushUpState = PushUpState.START
private var totalScore: Float = 0f
private var frameCount: Int = 0
private var evaluationMessages: MutableList<EvaluationMessage> = mutableListOf()
private var repCount: Int = 0
private var isRepCounting: Boolean = false
// 俯卧撑关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
private val ELBOW_ANGLE_START_MIN = 170f // 起始姿态(手臂伸直,放宽要求)
private val ELBOW_ANGLE_BOTTOM_MAX = 100f // 底部姿态(手肘弯曲程度,放宽要求)
private val HIP_SHOULDER_ANKLE_MIN = 160f // 身体直线(髋-肩-踝)
private val HIP_SHOULDER_ANKLE_MAX = 185f // 允许略微的弧度
// Helper function to calculate angle between three keypoints (A-B-C)
// B is the vertex of the angle
private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
return B.abttPoints(A, B, C)
}
override fun evaluateFrame(keyPoints: List<KeyPoint>) {
if (keyPoints.isEmpty()) {
evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
return
}
frameCount++
// 获取俯卧撑所需关键点
val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
val leftElbow = keyPoints.find { it.bodyPart == BodyPart.LEFT_ELBOW }
val rightElbow = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ELBOW }
val leftWrist = keyPoints.find { it.bodyPart == BodyPart.LEFT_WRIST }
val rightWrist = keyPoints.find { it.bodyPart == BodyPart.RIGHT_WRIST }
val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
val leftAnkle = keyPoints.find { it.bodyPart == BodyPart.LEFT_ANKLE }
val rightAnkle = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ANKLE }
// 确保所有关键点都存在
if (leftShoulder == null || rightShoulder == null || leftElbow == null || rightElbow == null ||
leftWrist == null || rightWrist == null || leftHip == null || rightHip == null ||
leftAnkle == null || rightAnkle == null) {
evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
return
}
// 计算左右平均关键点,提高稳定性
val midShoulder = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
(leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
),
(leftShoulder.score + rightShoulder.score) / 2
)
val midElbow = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftElbow.coordinate.x + rightElbow.coordinate.x) / 2,
(leftElbow.coordinate.y + rightElbow.coordinate.y) / 2
),
(leftElbow.score + rightElbow.score) / 2
)
val midWrist = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftWrist.coordinate.x + rightWrist.coordinate.x) / 2,
(leftWrist.coordinate.y + rightWrist.coordinate.y) / 2
),
(leftWrist.score + rightWrist.score) / 2
)
val midHip = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftHip.coordinate.x + rightHip.coordinate.x) / 2,
(leftHip.coordinate.y + rightHip.coordinate.y) / 2
),
(leftHip.score + rightHip.score) / 2
)
val midAnkle = KeyPoint(
BodyPart.NOSE,
android.graphics.PointF(
(leftAnkle.coordinate.x + rightAnkle.coordinate.x) / 2,
(leftAnkle.coordinate.y + rightAnkle.coordinate.y) / 2
),
(leftAnkle.score + rightAnkle.score) / 2
)
// 计算核心角度
val elbowAngle = calculateAngle(midShoulder, midElbow, midWrist) // 肩-肘-腕
val bodyLineAngle = calculateAngle(midShoulder, midHip, midAnkle) // 肩-髋-踝
var frameScore = 0f
var frameEvaluation = ""
var isBodyStraight = true
// 检查身体是否呈直线
if (bodyLineAngle !in HIP_SHOULDER_ANKLE_MIN..HIP_SHOULDER_ANKLE_MAX) {
isBodyStraight = false
if (midHip.coordinate.y < midShoulder.coordinate.y) { // 臀部过高
evaluationMessages.add(EvaluationMessage("臀部抬得太高了,尝试放低臀部,保持身体呈一条直线。", true))
} else if (midHip.coordinate.y > midAnkle.coordinate.y) { // 臀部下沉
evaluationMessages.add(EvaluationMessage("臀部下沉了,收紧核心,将臀部向上抬起,保持身体的平直。", true))
} else {
evaluationMessages.add(EvaluationMessage("身体不够平直,请调整髋部位置。", true))
}
}
when (currentState) {
PushUpState.START -> {
// 评估起始姿态 (手臂伸直,身体呈直线)
if (elbowAngle > ELBOW_ANGLE_START_MIN - 10 && isBodyStraight) { // 稍微放宽手臂伸直要求
frameEvaluation = "起始姿态良好,手臂基本伸直,身体呈一条直线。"
isRepCounting = false
currentState = PushUpState.DESCENT
frameScore = 200f // 提高分数
} else {
if (elbowAngle < ELBOW_ANGLE_START_MIN - 10) evaluationMessages.add(EvaluationMessage("手臂未完全伸直,请确保回到起始位置。", true))
if (!isBodyStraight) evaluationMessages.add(EvaluationMessage("身体未保持一条直线,请调整核心,不要塌腰或撅臀。", true))
frameEvaluation = "请调整至正确起始姿态。"
}
}
PushUpState.DESCENT -> {
// 评估下降过程
if (elbowAngle < ELBOW_ANGLE_START_MIN - 15) { // 手肘开始弯曲
if (isBodyStraight) {
frameEvaluation = "下降得很稳,保持身体平直。"
currentState = PushUpState.BOTTOM
frameScore = 400f // 提高分数
} else {
evaluationMessages.add(EvaluationMessage("下降时身体不够平直,请调整核心,保持身体直线。", true))
frameEvaluation = "下降中,注意保持身体直线。"
}
} else {
frameEvaluation = "继续下降,感受胸部拉伸。"
}
}
PushUpState.BOTTOM -> {
// 评估底部姿态 (胸部接近地面,手肘弯曲到位,身体呈直线)
if (elbowAngle < ELBOW_ANGLE_BOTTOM_MAX + 10 && isBodyStraight) { // 放宽手肘弯曲
frameEvaluation = "底部姿态完美,胸部接近地面!"
currentState = PushUpState.ASCENT
frameScore = 600f // 提高分数
} else {
if (elbowAngle > ELBOW_ANGLE_BOTTOM_MAX + 10) evaluationMessages.add(EvaluationMessage("下降不够深,请尝试让胸部更接近地面。", true))
if (!isBodyStraight) evaluationMessages.add(EvaluationMessage("底部姿态身体不直,请调整。", true))
frameEvaluation = "底部姿态可改进。"
}
}
PushUpState.ASCENT -> {
// 评估上推过程
if (elbowAngle > ELBOW_ANGLE_BOTTOM_MAX + 15) { // 手肘开始伸直
if (isBodyStraight) {
frameEvaluation = "上推有力,身体保持平直。"
currentState = PushUpState.LOCKOUT
frameScore = 400f // 提高分数
} else {
evaluationMessages.add(EvaluationMessage("上推时身体不够平直,请调整核心,保持身体直线。", true))
frameEvaluation = "上推中,注意保持身体直线。"
}
} else {
frameEvaluation = "继续上推,感受胸部和手臂发力。"
}
}
PushUpState.LOCKOUT -> {
// 评估完全推起(锁定)姿态
if (elbowAngle > ELBOW_ANGLE_START_MIN - 15 && isBodyStraight) { // 稍微放宽手臂伸直要求
frameEvaluation = "完美完成一次俯卧撑!手臂基本伸直,身体呈一条直线。"
if (!isRepCounting) {
repCount++
isRepCounting = true
evaluationMessages.add(EvaluationMessage("恭喜你,又完成了一次俯卧撑!目前累计完成了 $repCount 次,继续保持!"))
}
currentState = PushUpState.START // 完成一次后回到起始状态,准备下一次
frameScore = 200f // 提高分数
} else {
if (elbowAngle < ELBOW_ANGLE_START_MIN - 15) evaluationMessages.add(EvaluationMessage("手臂未完全伸直,请确保回到起始位置。", true))
if (!isBodyStraight) evaluationMessages.add(EvaluationMessage("锁定姿态身体不直,请调整。", true))
frameEvaluation = "请完成锁定:手臂基本伸直,身体保持平直。"
}
}
}
totalScore += frameScore
evaluationMessages.add(EvaluationMessage(frameEvaluation))
}
override fun getFinalScore(): Float {
val rawScore = if (frameCount > 0) totalScore / frameCount else 0f
val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
return roundedScore.coerceIn(0f, 100f)
}
override fun getFinalEvaluation(finalScore: Float): String {
val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
val overallEvaluationBuilder = StringBuilder()
if (errors.isEmpty()) {
when (finalScore.toInt()) {
in 90..100 -> overallEvaluationBuilder.append("太棒了!你的俯卧撑动作几乎完美无瑕,姿态标准,力量十足!继续保持!")
in 70..89 -> overallEvaluationBuilder.append("非常不错的俯卧撑!动作基本流畅,姿态也比较到位,再稍加注意细节就能更完美!")
in 50..69 -> overallEvaluationBuilder.append("俯卧撑动作有进步空间哦!虽然有些地方做得不错,但还需要多练习,让姿态更稳定、发力更集中。")
in 30..49 -> overallEvaluationBuilder.append("本次俯卧撑需要更多练习。动作中存在一些明显的姿态问题,这会影响训练效果和安全性。")
else -> overallEvaluationBuilder.append("俯卧撑动作仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
}
} else {
overallEvaluationBuilder.append("本次俯卧撑分析完成!发现了一些可以改进的地方:\n")
overallEvaluationBuilder.append(errors.joinToString("\n"))
overallEvaluationBuilder.append("\n总次数:$repCount")
}
overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
overallEvaluationBuilder.append(uniqueMessages)
return overallEvaluationBuilder.toString()
}
}

@ -1,218 +0,0 @@
package org.tensorflow.lite.examples.poseestimation.evaluator
import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
import org.tensorflow.lite.examples.poseestimation.data.BodyPart
import kotlin.math.abs
import kotlin.math.roundToInt
// 定义深蹲动作的阶段
enum class SquatState {
START, // 起始姿态(站立)
DESCENT, // 下蹲过程
BOTTOM, // 底部姿态(深蹲最低点)
ASCENT, // 起身过程
LOCKOUT // 锁定(完成站立)姿态
}
class SquatEvaluator : ExerciseEvaluator {
private var currentState: SquatState = SquatState.START
private var totalScore: Float = 0f
private var frameCount: Int = 0
private var evaluationMessages: MutableList<EvaluationMessage> = mutableListOf()
private var repCount: Int = 0
private var isRepCounting: Boolean = false
// 深蹲关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
private val HIP_ANGLE_START_MAX = 175f // 站立时髋关节角度上限
private val KNEE_ANGLE_START_MAX = 175f // 站立时膝关节角度上限
private val TORSO_ANGLE_START_MAX = 15f // 站立时躯干与垂直方向最大夹角(微前倾)
private val HIP_ANGLE_BOTTOM_MIN = 60f // 深蹲底部髋关节最小角度(深蹲深度)
private val KNEE_ANGLE_BOTTOM_MIN = 70f // 深蹲底部膝关节最小角度(深蹲深度)
private val TORSO_ANGLE_BOTTOM_MAX = 45f // 深蹲底部躯干与垂直方向最大夹角(通常会前倾)
private val BACK_STRAIGHT_THRESHOLD = 160f // 躯干挺直角度阈值 (肩-髋-膝角度,用于判断背部是否弓起)
// Helper function to calculate angle between three keypoints (A-B-C)
// B is the vertex of the angle
private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
return B.abttPoints(A, B, C)
}
override fun evaluateFrame(keyPoints: List<KeyPoint>) {
if (keyPoints.isEmpty()) {
evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
return
}
frameCount++
// 获取深蹲所需关键点
val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
val leftKnee = keyPoints.find { it.bodyPart == BodyPart.LEFT_KNEE }
val rightKnee = keyPoints.find { it.bodyPart == BodyPart.RIGHT_KNEE }
val leftAnkle = keyPoints.find { it.bodyPart == BodyPart.LEFT_ANKLE }
val rightAnkle = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ANKLE }
// 确保所有关键点都存在,如果缺失,则跳过此帧或给出警告
if (leftShoulder == null || rightShoulder == null || leftHip == null || rightHip == null ||
leftKnee == null || rightKnee == null || leftAnkle == null || rightAnkle == null) {
evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
return
}
// 计算左右平均关键点,提高稳定性
val midShoulder = KeyPoint(
BodyPart.NOSE, // 使用 NOSE 作为占位符
android.graphics.PointF(
(leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
(leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
),
(leftShoulder.score + rightShoulder.score) / 2
)
val midHip = KeyPoint(
BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
android.graphics.PointF(
(leftHip.coordinate.x + rightHip.coordinate.x) / 2,
(leftHip.coordinate.y + rightHip.coordinate.y) / 2
),
(leftHip.score + rightHip.score) / 2
)
val midKnee = KeyPoint(
BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
android.graphics.PointF(
(leftKnee.coordinate.x + rightKnee.coordinate.x) / 2,
(leftKnee.coordinate.y + rightKnee.coordinate.y) / 2
),
(leftKnee.score + rightKnee.score) / 2
)
val midAnkle = KeyPoint(
BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
android.graphics.PointF(
(leftAnkle.coordinate.x + rightAnkle.coordinate.x) / 2,
(leftAnkle.coordinate.y + rightAnkle.coordinate.y) / 2
),
(leftAnkle.score + rightAnkle.score) / 2
)
// 计算核心角度
val hipAngle = calculateAngle(midShoulder, midHip, midKnee) // 肩-髋-膝
val kneeAngle = calculateAngle(midHip, midKnee, midAnkle) // 髋-膝-踝
// 躯干角度通过肩和髋的y坐标差与x坐标差的反正切并转换为相对于垂直线的角度
val dxTorso = midHip.coordinate.x - midShoulder.coordinate.x
val dyTorso = midHip.coordinate.y - midShoulder.coordinate.y
val torsoAngleVertical = Math.toDegrees(kotlin.math.atan2(dxTorso.toDouble(), dyTorso.toDouble())).toFloat()
val realTorsoAngle = abs(torsoAngleVertical) // 简化处理,取绝对值
var frameScore = 0f
var frameEvaluation = ""
when (currentState) {
SquatState.START -> {
if (hipAngle > HIP_ANGLE_START_MAX - 5 && kneeAngle > KNEE_ANGLE_START_MAX - 5 && realTorsoAngle < TORSO_ANGLE_START_MAX + 5) {
frameEvaluation = "起始姿态良好,准备下蹲。"
isRepCounting = false
currentState = SquatState.DESCENT
frameScore = 300f
} else {
frameEvaluation = "请调整至正确起始姿态:站直,核心收紧。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
}
SquatState.DESCENT -> {
if (hipAngle < HIP_ANGLE_START_MAX && kneeAngle < KNEE_ANGLE_START_MAX) {
if (realTorsoAngle < TORSO_ANGLE_BOTTOM_MAX + 15) { // 下降过程中背部不应过度前倾
frameEvaluation = "下降得很稳,保持背部挺直。"
currentState = SquatState.BOTTOM
frameScore = 300f
} else {
frameEvaluation = "下蹲时背部有点弓起或过度前倾。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
} else {
frameEvaluation = "继续下蹲,感受臀腿发力。"
}
}
SquatState.BOTTOM -> {
if (hipAngle >= HIP_ANGLE_BOTTOM_MIN && kneeAngle >= KNEE_ANGLE_BOTTOM_MIN && realTorsoAngle <= TORSO_ANGLE_BOTTOM_MAX) {
frameEvaluation = "底部姿态非常棒,深蹲深度足够!"
currentState = SquatState.ASCENT
frameScore = 450f
} else {
frameEvaluation = "底部姿态可改进。"
if (hipAngle < HIP_ANGLE_BOTTOM_MIN) evaluationMessages.add(EvaluationMessage("深蹲深度不足,请尝试蹲得更深。", true))
if (kneeAngle < KNEE_ANGLE_BOTTOM_MIN) evaluationMessages.add(EvaluationMessage("膝盖弯曲不足。", true))
if (realTorsoAngle > TORSO_ANGLE_BOTTOM_MAX) evaluationMessages.add(EvaluationMessage("背部过度前倾或弓起。", true))
}
}
SquatState.ASCENT -> {
if (hipAngle > HIP_ANGLE_BOTTOM_MIN && kneeAngle > KNEE_ANGLE_BOTTOM_MIN) {
if (realTorsoAngle < TORSO_ANGLE_START_MAX + 15) { // 上升过程中背部不应过度前倾
frameEvaluation = "起身有力,保持背部挺直。"
currentState = SquatState.LOCKOUT
frameScore = 300f
} else {
frameEvaluation = "起身时背部有点弓起或过度前倾。"
evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
}
} else {
frameEvaluation = "继续起身,将身体推回起始位置。"
}
}
SquatState.LOCKOUT -> {
if (hipAngle > HIP_ANGLE_START_MAX - 5 && kneeAngle > KNEE_ANGLE_START_MAX - 5 && realTorsoAngle < TORSO_ANGLE_START_MAX + 5) {
frameEvaluation = "完美完成一次深蹲!姿态非常棒!"
if (!isRepCounting) {
repCount++
isRepCounting = true
evaluationMessages.add(EvaluationMessage("恭喜你,又完成了一次深蹲!目前累计完成了 $repCount 次,继续保持!"))
}
currentState = SquatState.START
frameScore = 150f
} else {
frameEvaluation = "请完成锁定:髋部和膝盖完全伸展,全身收紧。"
if (hipAngle < HIP_ANGLE_START_MAX - 5 || kneeAngle < KNEE_ANGLE_START_MAX - 5) evaluationMessages.add(EvaluationMessage("顶部锁定还不够充分哦,髋部和膝盖可以再伸展一点。", true))
if (realTorsoAngle > TORSO_ANGLE_START_MAX + 5) evaluationMessages.add(EvaluationMessage("注意!在锁定阶段,身体可能有点过度后仰。保持核心收紧,稳稳地完成动作。", true))
}
}
}
totalScore += frameScore
evaluationMessages.add(EvaluationMessage(frameEvaluation))
}
override fun getFinalScore(): Float {
val rawScore = if (frameCount > 0) totalScore / frameCount else 0f
val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
return roundedScore.coerceIn(0f, 100f)
}
override fun getFinalEvaluation(finalScore: Float): String {
val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
val overallEvaluationBuilder = StringBuilder()
if (errors.isEmpty()) {
when (finalScore.toInt()) {
in 90..100 -> overallEvaluationBuilder.append("太棒了!你的深蹲动作几乎完美无瑕,姿态标准,力量十足!继续保持!")
in 70..89 -> overallEvaluationBuilder.append("非常不错的深蹲!动作基本流畅,姿态也比较到位,再稍加注意细节就能更完美!")
in 50..69 -> overallEvaluationBuilder.append("深蹲动作有进步空间哦!虽然有些地方做得不错,但还需要多练习,让姿态更稳定、发力更集中。")
in 30..49 -> overallEvaluationBuilder.append("本次深蹲需要更多练习。动作中存在一些明显的姿态问题,这会影响训练效果和安全性。")
else -> overallEvaluationBuilder.append("深蹲动作仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
}
} else {
overallEvaluationBuilder.append("本次深蹲分析完成!发现了一些可以改进的地方:\n")
overallEvaluationBuilder.append(errors.joinToString("\n"))
overallEvaluationBuilder.append("\n总次数:$repCount")
}
overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
overallEvaluationBuilder.append(uniqueMessages)
return overallEvaluationBuilder.toString()
}
}

@ -45,12 +45,10 @@ class PoseNet(private val interpreter: Interpreter, private var gpuDelegate: Gpu
companion object {
// 设置CPU线程数
private const val CPU_NUM_THREADS = 4
// 图像标准化时的均值和标准差
private const val MEAN = 127.5f
private const val STD = 127.5f
private const val TAG = "Posenet"
// 模型文件名称
private const val MODEL_FILENAME = "posenet.tflite"
@ -62,12 +60,10 @@ class PoseNet(private val interpreter: Interpreter, private var gpuDelegate: Gpu
when (device) {
Device.CPU -> {
}
Device.GPU -> {
gpuDelegate = GpuDelegate() // 使用GPU加速
options.addDelegate(gpuDelegate)
}
Device.NNAPI -> options.setUseNNAPI(true) // 使用NNAPI加速
}
return PoseNet(
@ -81,7 +77,6 @@ class PoseNet(private val interpreter: Interpreter, private var gpuDelegate: Gpu
// 保存上一次推理的时间
private var lastInferenceTimeNanos: Long = -1
// 获取输入张量的宽度和高度
private val inputWidth = interpreter.getInputTensor(0).shape()[1]
private val inputHeight = interpreter.getInputTensor(0).shape()[2]
@ -145,9 +140,8 @@ class PoseNet(private val interpreter: Interpreter, private var gpuDelegate: Gpu
fun calculateAngle(x1: Float, y1: Float, x2: Float, y2: Float): Float {
val Y = y2 - y1
val X = x2 - x1
return Math.toDegrees(atan2(Y.toDouble(), X.toDouble())).toFloat()
return Math.toDegrees(atan2(Y.toDouble(),X.toDouble())).toFloat()
}
private fun postProcessModelOuputs(
heatmaps: Array<Array<Array<FloatArray>>>,
offsets: Array<Array<Array<FloatArray>>>
@ -237,7 +231,7 @@ class PoseNet(private val interpreter: Interpreter, private var gpuDelegate: Gpu
return Math.toDegrees(acos(cosAngle.toDouble())).toFloat()
}
// 添加角度
// 添加角度并控制角度列表长度
fun addAngle(angle: Float) {
angles.add(angle)
if (angles.size > 8) {
@ -261,24 +255,20 @@ class PoseNet(private val interpreter: Interpreter, private var gpuDelegate: Gpu
val leftWrist = keypointList.first { it.bodyPart == BodyPart.LEFT_WRIST }
calculateAndAddAngle(leftShoulder, leftElbow, leftWrist)
val rightShoulder =
keypointList.first { it.bodyPart == BodyPart.RIGHT_SHOULDER }
val rightShoulder = keypointList.first { it.bodyPart == BodyPart.RIGHT_SHOULDER }
val rightElbow = keypointList.first { it.bodyPart == BodyPart.RIGHT_ELBOW }
calculateAndAddAngle(leftShoulder, rightShoulder, rightElbow)
}
BodyPart.RIGHT_SHOULDER -> {
val rightShoulder = keypoint
val rightElbow = keypointList.first { it.bodyPart == BodyPart.RIGHT_ELBOW }
val rightWrist = keypointList.first { it.bodyPart == BodyPart.RIGHT_WRIST }
calculateAndAddAngle(rightShoulder, rightElbow, rightWrist)
val leftShoulder =
keypointList.first { it.bodyPart == BodyPart.LEFT_SHOULDER }
val leftShoulder = keypointList.first { it.bodyPart == BodyPart.LEFT_SHOULDER }
val leftElbow = keypointList.first { it.bodyPart == BodyPart.LEFT_ELBOW }
calculateAndAddAngle(rightShoulder, leftShoulder, leftElbow)
}
BodyPart.LEFT_HIP -> {
val leftHip = keypoint
val rightHip = keypointList.first { it.bodyPart == BodyPart.RIGHT_HIP }
@ -291,7 +281,6 @@ class PoseNet(private val interpreter: Interpreter, private var gpuDelegate: Gpu
val leftAnkle = keypointList.first { it.bodyPart == BodyPart.LEFT_ANKLE }
calculateAndAddAngle(leftHip, leftKnee, leftAnkle)
}
BodyPart.RIGHT_HIP -> {
val rightHip = keypoint
val leftHip = keypointList.first { it.bodyPart == BodyPart.LEFT_HIP }
@ -301,23 +290,14 @@ class PoseNet(private val interpreter: Interpreter, private var gpuDelegate: Gpu
val rightAnkle = keypointList.first { it.bodyPart == BodyPart.RIGHT_ANKLE }
calculateAndAddAngle(rightHip, rightKnee, rightAnkle)
}
// 这里可以继续添加其他 BodyPart 的处理分支
else -> {
// 如果遇到其他没有显式列出的bodyPart进行处理
println("Unknown body part: ${keypoint.bodyPart}")
}
}
}
}
// 提供外部访问 angles 的方法
val getAngles: List<Float> get(){
return angles.toList() // 返回 angles 的副本
}
// 可以选择提供修改 angles 的方法
fun clearAngles() {
angles.clear()
}
}
// 返回上一次推理的时间

@ -0,0 +1,40 @@
package org.tensorflow.lite.examples.poseestimation.utils;
import android.util.Base64;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class PasswordUtils {
private static final String ALGORITHM = "SHA-256";
private static final String SECRET_KEY = "YourSecretKey123"; // 实际应用中应存储在安全位置
// 生成密码哈希
public static String hashPassword(String password) {
try {
MessageDigest digest = MessageDigest.getInstance(ALGORITHM);
byte[] encodedHash = digest.digest(password.getBytes());
StringBuilder hexString = new StringBuilder(2 * encodedHash.length);
for (byte b : encodedHash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
// 验证密码
public static boolean verifyPassword(String password, String hashedPassword) {
String hashedInput = hashPassword(password);
return hashedInput != null && hashedInput.equals(hashedPassword);
}
}

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#222" />
<stroke android:width="4dp" android:color="#FFA500" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 B

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#555555"/>
<size android:width="100dp" android:height="100dp"/>
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 753 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 B

@ -1,248 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1C1C1E"
android:orientation="vertical">
<!-- 顶部标题栏 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:paddingHorizontal="16dp">
<ImageView
android:id="@+id/btn_back"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_centerVertical="true"
android:src="@drawable/ic_back"
app:tint="#FFF"
android:contentDescription="Back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Edit Profile"
android:textColor="#FFF"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>
<!-- 头像区域 -->
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp">
<ImageView
android:id="@+id/image_avatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@drawable/avatar_circle_bg"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_image"
android:contentDescription="User Avatar"/> <!-- 默认头像 -->
<ImageView
android:id="@+id/btn_select_photo"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="bottom|end"
android:layout_margin="4dp"
android:background="@drawable/circle_button_background"
android:padding="8dp"
android:src="@drawable/ic_back"
app:tint="#FFF"
android:contentDescription="Select Photo"/> <!-- 注意这里的ic_back需要替换为你项目中实际的相机图标资源例如camera.png -->
</FrameLayout>
<!-- 用户名和个性签名输入框 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="vertical"
android:paddingHorizontal="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户名"
android:textColor="#AAA"
android:textSize="14sp" />
<EditText
android:id="@+id/edit_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@android:color/transparent"
android:textColor="#FFF"
android:textSize="18sp"
android:enabled="false"
android:focusable="false"
android:focusableInTouchMode="false"/> <!-- 用户名暂时设置为不可编辑 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="#333" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="个性签名"
android:textColor="#AAA"
android:textSize="14sp" />
<EditText
android:id="@+id/edit_signature"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@android:color/transparent"
android:textColor="#FFF"
android:textSize="18sp"
android:hint="添加个性签名"
android:textColorHint="#555"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="#333" />
<!-- 性别输入框 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="性别"
android:textColor="#AAA"
android:textSize="14sp" />
<EditText
android:id="@+id/edit_gender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@android:color/transparent"
android:textColor="#FFF"
android:textSize="18sp"
android:hint="请输入性别 (男/女)"
android:textColorHint="#555"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="#333" />
<!-- 年龄输入框 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="年龄"
android:textColor="#AAA"
android:textSize="14sp" />
<EditText
android:id="@+id/edit_age"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@android:color/transparent"
android:textColor="#FFF"
android:textSize="18sp"
android:hint="请输入年龄"
android:inputType="number"
android:textColorHint="#555"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="#333" />
<!-- 体重输入框 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="体重"
android:textColor="#AAA"
android:textSize="14sp" />
<EditText
android:id="@+id/edit_weight"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@android:color/transparent"
android:textColor="#FFF"
android:textSize="18sp"
android:hint="请输入体重 (kg)"
android:inputType="numberDecimal"
android:textColorHint="#555"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="#333" />
<!-- 身高输入框 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="身高"
android:textColor="#AAA"
android:textSize="14sp" />
<EditText
android:id="@+id/edit_height"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@android:color/transparent"
android:textColor="#FFF"
android:textSize="18sp"
android:hint="请输入身高 (cm)"
android:inputType="numberDecimal"
android:textColorHint="#555"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="#333" />
</LinearLayout>
<!-- 保存按钮 -->
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
android:text="Save"
android:textColor="#FFF"
android:textSize="18sp"
app:cornerRadius="28dp" />
</LinearLayout>

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1C1C1E"
tools:context=".ExerciseDetailActivity">
<!-- 顶部的图片区域 -->
<ImageView
android:id="@+id/exercise_detail_image"
android:layout_width="match_parent"
android:layout_height="250dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_image" /> <!-- 这里的图片可以根据具体动作设置 -->
<!-- 返回按钮 -->
<ImageButton
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_back" /> <!-- 需要一个返回图标资源 -->
<!-- 动作名称 -->
<TextView
android:id="@+id/exercise_detail_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/exercise_detail_image"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:textColor="#FFFFFF"
android:textSize="22sp"
android:textStyle="bold"
android:text="动作名称" />
<!-- 动作描述/注意事项 -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/exercise_detail_name"
android:layout_above="@id/start_training_button"
android:layout_marginTop="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/exercise_detail_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#CCCCCC"
android:textSize="18sp"
android:text="这里是动作的详细描述和注意事项..." />
</ScrollView>
<!-- 开始训练按钮 -->
<Button
android:id="@+id/start_training_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:backgroundTint="#A020F0"
android:text="开始训练"
android:textColor="#FFFFFF"
android:textSize="18sp"/>
</RelativeLayout>

@ -10,18 +10,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- 用于显示姿态建议的 TextView -->
<TextView
android:id="@+id/tv_pose_advice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:gravity="center_horizontal"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:text="姿态建议将显示在这里"
android:background="#80000000" /> <!-- 半透明黑色背景 -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainTabActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#222222"
app:titleTextColor="#FFFFFF">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="形动力"
android:textColor="#FFBB86FC"
android:textSize="20sp"
android:textStyle="bold" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="56dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<LinearLayout
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_gravity="bottom"
android:orientation="horizontal"
android:background="#222222">
<ImageView
android:id="@+id/nav_home"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@drawable/home1"
android:scaleType="centerInside" />
<ImageView
android:id="@+id/nav_data"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@drawable/data2"
android:scaleType="centerInside" />
<ImageView
android:id="@+id/nav_setting"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@drawable/setting2"
android:scaleType="centerInside" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1C1C1E"
tools:context=".VideoAnalysisActivity">
<TextView
android:id="@+id/tv_exercise_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="32dp"
android:text="动作名称"
android:textColor="#FFFFFF"
android:textSize="24sp"
android:textStyle="bold" />
<Button
android:id="@+id/btn_select_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="选择本地视频"
android:textColor="#FFFFFF"
android:backgroundTint="#A020F0"
android:textSize="18sp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingTop="12dp"
android:paddingBottom="12dp" />
<TextView
android:id="@+id/tv_analysis_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/btn_select_video"
android:layout_centerHorizontal="true"
android:layout_marginTop="24dp"
android:textColor="#CCCCCC"
android:textSize="16sp"
android:text="请选择视频进行分析..."
android:visibility="gone"/>
<ProgressBar
android:id="@+id/progress_bar_analysis"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/tv_analysis_status"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginTop="16dp"
android:progressTint="#A020F0"
android:visibility="gone" />
</RelativeLayout>

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1C1C1E"
tools:context=".VideoAnalysisResultActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_result_exercise_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="动作名称"
android:textColor="#FFFFFF"
android:textSize="24sp"
android:textStyle="bold"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/tv_result_score_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="最终得分: "
android:textColor="#CCCCCC"
android:textSize="18sp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tv_result_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="N/A"
android:textColor="#A020F0"
android:textSize="36sp"
android:textStyle="bold"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="24dp"/>
<TextView
android:id="@+id/tv_result_evaluation_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="详细评价: "
android:textColor="#CCCCCC"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tv_result_evaluation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="分析中..."
android:textColor="#FFFFFF"
android:textSize="16sp"
android:lineSpacingExtra="4dp"/>
</LinearLayout>
</ScrollView>

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1C1C1E"
tools:context=".DataFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_video_analysis_results"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:clipToPadding="false"
android:background="#1C1C1E"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1C1C1E">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- 硬拉卡片 -->
<include
android:id="@+id/deadlift_card"
layout="@layout/item_exercise_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<!-- 深蹲卡片 -->
<include
android:id="@+id/squat_card"
layout="@layout/item_exercise_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<!-- 平板支撑卡片 -->
<include
android:id="@+id/plank_card"
layout="@layout/item_exercise_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<!-- 引体向上卡片 -->
<include
android:id="@+id/pullup_card"
layout="@layout/item_exercise_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<!-- 俯卧撑卡片 -->
<include
android:id="@+id/pushup_card"
layout="@layout/item_exercise_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
</LinearLayout>
</ScrollView>

@ -1,139 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#1C1C1E"
android:padding="24dp">
<!-- 用户头像和信息区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingBottom="24dp">
<ImageView
android:id="@+id/image_avatar"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/placeholder_image"
android:background="@drawable/avatar_circle_bg"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="16dp">
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户名"
android:textColor="#FFF"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_signature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="个性签名"
android:textColor="#AAA"
android:textSize="20sp" />
</LinearLayout>
</LinearLayout>
<!-- 菜单区 -->
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#333" />
<!-- 个人信息按钮和展开区包裹在一起 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/btn_personal_info"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:text="个人信息"
android:textColor="#FFF"
android:textSize="20sp" />
<!-- 个人信息展开区 -->
<LinearLayout
android:id="@+id/layout_personal_detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
android:padding="12dp"
android:background="#222">
<TextView android:id="@+id/tv_gender" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="性别:" android:textColor="#FFF" android:textSize="18sp" />
<TextView android:id="@+id/tv_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="年龄:" android:textColor="#FFF" android:textSize="18sp" />
<TextView android:id="@+id/tv_weight" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="体重:" android:textColor="#FFF" android:textSize="18sp" />
<TextView android:id="@+id/tv_height" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="身高:" android:textColor="#FFF" android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
<!-- 新增的 我的数据 栏 -->
<TextView
android:id="@+id/btn_my_data"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:text="我的数据"
android:textColor="#FFF"
android:textSize="20sp" />
<LinearLayout
android:id="@+id/layout_my_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:background="#222"
android:visibility="gone">
<TextView android:id="@+id/tv_total_trainings" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="总训练次数0" android:textColor="#FFF" android:textSize="18sp" />
<TextView android:id="@+id/tv_total_calories" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="总消耗卡路里0 kcal" android:textColor="#FFF" android:textSize="18sp" />
<TextView android:id="@+id/tv_total_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="总训练时间0 分钟" android:textColor="#FFF" android:textSize="18sp" />
</LinearLayout>
<!-- 账户信息和设置按钮 -->
<TextView
android:id="@+id/btn_account_info"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:text="账户信息"
android:textColor="#FFF"
android:textSize="20sp" />
<TextView
android:id="@+id/btn_setting"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:text="设置"
android:textColor="#FFF"
android:textSize="20sp" />
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#333" />
<View android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" />
<!-- 注销按钮 -->
<TextView
android:id="@+id/btn_logout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center"
android:text="注销"
android:textColor="#FF3B30"
android:textSize="20sp"
android:background="?android:attr/selectableItemBackground" />
</LinearLayout>

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#333333">
<ImageView
android:id="@+id/exercise_image"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/exercise_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textColor="#FFFFFF"
android:textSize="18sp" />
<TextView
android:id="@+id/exercise_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textColor="#AAAAAA"
android:textSize="14sp"
android:visibility="gone" />
</LinearLayout>
</androidx.cardview.widget.CardView>

@ -1,71 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardBackgroundColor="#222222">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:padding="8dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/video_thumbnail"
android:layout_width="100dp"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_image"
android:background="#000000"
android:contentDescription="视频缩略图"
android:clickable="true"
android:focusable="true" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<TextView
android:id="@+id/tv_exercise_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运动类型:硬拉"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_evaluation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:maxLines="3"
android:ellipsize="end"
android:text="评价:动作完成度很高,但下放深度不足。请注意控制节奏。"
android:textColor="#AAAAAA"
android:textSize="13sp" />
</LinearLayout>
<TextView
android:id="@+id/tv_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="85 分"
android:textColor="#FFBB86FC"
android:textSize="28sp"
android:textStyle="bold"
android:gravity="center_vertical"/>
</LinearLayout>
</androidx.cardview.widget.CardView>

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_videos" path="."/>
</paths>

@ -4,5 +4,10 @@
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
<<<<<<< HEAD
#Mon May 26 14:34:26 GMT+08:00 2025
sdk.dir=D\:\\wangjiachen\\sdk
=======
#Sat May 24 23:39:55 CST 2025
sdk.dir=/Users/ziyue/Library/Android/sdk
sdk.dir=C\:\\Users\\26891\\AppData\\Local\\Android\\Sdk
>>>>>>> a11e684a7b53b2288bd2435ed63bc73bcd82db22

Loading…
Cancel
Save