Android पर लाइव वॉलपेपर के लिए लंबन प्रभाव

हर कोई जिसने लाइव वॉलपेपर इंस्टॉल करने की कोशिश की, ने डेस्कटॉप के बीच चलते समय एक लंबन प्रभाव देखा। यह बहुत दिलचस्प लग रहा है, लेकिन इसके कार्यान्वयन में ऐसी समस्याएं हैं जिन्हें इस लेख में हाइलाइट किया जाएगा। हम एंड्रॉइड लाइव वॉलपेपर के लिए लंबन प्रभाव के कार्यान्वयन के बारे में बात करेंगे।


नीचे हम मानक और कस्टम कार्यान्वयन विधियों पर विचार करेंगे। उनमें से प्रत्येक के नुकसान और फायदे का संकेत दिया गया है।

मानक विधि


API7 के साथ शुरू, WallpaperService.Engine वर्ग ऑनऑफसेट्सचेंज विधि के साथ दिखाई दिया। हर बार जब डेस्कटॉप अपनी स्थिति बदलता है तो इस विधि को कहा जाता है। इसका उपयोग करने के लिए, यह WallpaperService.Engine वर्ग के स्वयं के कार्यान्वयन में इसे ओवरराइड करने के लिए पर्याप्त है। विधि में निम्नलिखित हस्ताक्षर हैं:

onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset) 


सभी स्थानांतरित मापदंडों में से, हम xOffset और yOffset में रुचि रखते हैं, और लाइव वॉलपेपर के संबंध में, यह xOffset का उपयोग करने के लिए पर्याप्त है। यह पैरामीटर 0 से 1 तक भिन्न होता है, डेस्कटॉप के एक चरम स्थान पर 0 और डेस्कटॉप के दूसरे चरम स्थान पर 1 के बराबर होता है। यदि डेस्कटॉप डिफ़ॉल्ट स्थिति (मध्य में) में है, तो xOffset पैरामीटर 0.5 है। उदाहरण के लिए, 3 डेस्कटॉप के लिए, xOffset क्रमशः 0, 0.5, 1 होगा। जब एक डेस्कटॉप से ​​दूसरे में जा रहा है, तो पैरामीटर सुचारू रूप से बदल जाता है , और onOffsetsChanged विधि को बार-बार कहा जाता है। हालाँकि, विभिन्न उपकरणों पर "चिकनाई" भिन्न हो सकती है।

इस प्रकार, अपने वॉलपेपर के रेंडरर को इस पैरामीटर को पास करते हुए, आप लंबन प्रभाव का एहसास करते हुए, उन्हें सही दिशा में स्थानांतरित कर सकते हैं। लाभ स्पष्ट हैं: एक न्यूनतम कोड और डेस्कटॉप के साथ तुल्यकालिक काम।

इस विधि के नुकसान के लिए नहीं तो सब कुछ ठीक हो जाएगा:


मूल विधि, ZTouchMove कक्षा


इन सभी समस्याओं के कारण, यह निर्णय लिया गया कि सभी उपकरणों पर चलेगा। इसके लिए, उसी WallpaperService.Engine वर्ग का ऑनटचिव तरीका पाया गया। इस विधि का उपयोग करने के लिए, आपको पहले इसकी कॉल को सक्षम करना होगा:
 @Override public void onCreate(SurfaceHolder surfaceHolder) { setTouchEventsEnabled(true); } 


इसके अलावा, यह विधि स्क्रीन को छूने से संबंधित सभी घटनाओं को स्वीकार करेगी। हालाँकि, मैं स्पर्श को विस्थापन के पहले से ही पसंदीदा प्रारूप में 0 से 1 तक परिवर्तित करना चाहता हूं, जिसमें जड़ता, गति एनीमेशन और अन्य खुशियाँ शामिल हैं। इसके लिए, एक टच हैंडलर लिखा गया था, जिसने आउटपुट में "बिल्कुल वही दिया" जिसकी आवश्यकता है। नीचे दिए गए हैंडलर के लिए कोड है:

 import java.util.ArrayList; import java.util.Iterator; import java.util.List; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Point; import android.os.Build; import android.os.Handler; import android.view.Display; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.WindowManager; import android.view.animation.Interpolator; import android.widget.Scroller; public class ZTouchMove { public interface ZTouchMoveListener { public void onTouchOffsetChanged(float xOffset); } private List<ZTouchMoveListener> mListeners = new ArrayList<ZTouchMoveListener>(); public class ZInterpolator implements Interpolator { public float getInterpolation(float input) { // f(x) = ax^3 + bx^2 + cx + d // a = x - 2 // b = 3 - 2x // c = x // d = 0 // where x = derivative in point 0 //input = (float)(-Math.cos(10*((double)input/Math.PI)) + 1) / 2; input = (mVelocity - 2) * (float) Math.pow(input, 3) + (3 - 2 * mVelocity) * (float) Math.pow(input, 2) + mVelocity * input; return input; } } Handler mHandler = new Handler(); final Runnable mRunnable = new Runnable() { public void run() { if(onMovingToPosition()) mHandler.postDelayed(this, 20); } }; private float mPosition = 0.5f; private float mPositionDelta = 0; private float mTouchDownX; private int xDiff; private VelocityTracker mVelocityTracker; private float mVelocity = 0; private Scroller mScroller; private final static int TOUCH_STATE_REST = 0; private final static int TOUCH_STATE_SCROLLING = 1; private static final int SCROLLING_TIME = 300; private static final int SNAP_VELOCITY = 350; private int mTouchSlop; private int mMaximumVelocity; private int mTouchState = TOUCH_STATE_REST; private int mWidth; private int mNumVirtualScreens = 5; @SuppressLint("NewApi") @SuppressWarnings("deprecation") public void init(Context ctx) { mScroller = new Scroller(ctx, new ZInterpolator()); final ViewConfiguration configuration = ViewConfiguration.get(ctx); mTouchSlop = configuration.getScaledTouchSlop(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); // API Level 13 if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { Point size = new Point(); display.getSize(size); mWidth = size.x; } else { // API Level <13 mWidth = display.getWidth(); } } public void onTouchEvent(MotionEvent e) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(e); final float x = e.getX(); final int action = e.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING; if (!mScroller.isFinished()) { mScroller.abortAnimation(); } mTouchDownX = x; break; case MotionEvent.ACTION_MOVE: xDiff = (int) (x - mTouchDownX); if (Math.abs(xDiff) > mTouchSlop && mTouchState != TOUCH_STATE_SCROLLING) { mTouchState = TOUCH_STATE_SCROLLING; if(xDiff < 0) mTouchDownX = mTouchDownX - mTouchSlop; else mTouchDownX = mTouchDownX + mTouchSlop; xDiff = (int) (x - mTouchDownX); } if (mTouchState == TOUCH_STATE_SCROLLING) { mPositionDelta = -(float)xDiff / (mWidth * mNumVirtualScreens); } break; case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_SCROLLING) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); float velocityX = velocityTracker.getXVelocity() / (float)(mNumVirtualScreens * mWidth); mPosition = mPosition + mPositionDelta; mPositionDelta = 0; if(!returnSpring()) { mVelocity = Math.min(3, Math.abs(velocityX * mNumVirtualScreens)) ; // deaccelerate(); // Inertion if(Math.abs(velocityX) * (float)(mNumVirtualScreens * mWidth) > SNAP_VELOCITY) moveToPosition(mPosition, mPosition - (velocityX > 0 ? 1 : -1) * 1 / (float) mNumVirtualScreens ); else moveToPosition(mPosition, mPosition - 0.7f * velocityX * ((float)SCROLLING_TIME / 1000) ); } } mTouchState = TOUCH_STATE_REST; break; case MotionEvent.ACTION_CANCEL: mTouchState = TOUCH_STATE_REST; mPositionDelta = 0; break; } dispatchMoving(); } private boolean returnSpring() { mVelocity = 0; if(mPositionDelta + mPosition > 1 - 0.5 / (float) mNumVirtualScreens) moveToPosition(mPosition, (float) (1 - 0.5 / (float) mNumVirtualScreens)); else if(mPositionDelta + mPosition < 0.5 / (float) mNumVirtualScreens) moveToPosition(mPosition, (float) 0.5 / (float) mNumVirtualScreens); else return false; return true; } private void moveToPosition(float current_position, float desired_position) { mScroller.startScroll((int)(current_position * 1000), 0, (int)((desired_position - current_position) * 1000), 0, SCROLLING_TIME); mHandler.postDelayed(mRunnable, 20); } private boolean onMovingToPosition() { if(mScroller.computeScrollOffset()) { mPosition = (float)mScroller.getCurrX() / 1000; dispatchMoving(); return true; } else { returnSpring(); return false; } } private float normalizePosition(float xOffset) { final float springZone = 1 / (float) mNumVirtualScreens; // Normalized offset is from 0 to 0.5 float xOffsetNormalized = Math.abs(xOffset - 0.5f); if(xOffsetNormalized + springZone / 2 > 0.5f) { // Spring formula // (0.5 - 2 * (1 - (x / (2 * springZone) + 0.5))^2) * springZone // where x >=0 and <= springZone // delta y = springZone / 2, y >=0 and y <= springZone / 2 xOffsetNormalized = 0.5f - springZone / 2 + (0.5f - 2 * (float)Math.pow( (double)(1 - ( (xOffsetNormalized - 0.5f + springZone / 2) / (2 * springZone) + 0.5)), 2 ) ) * springZone; if(xOffset < 0.5f) xOffset = 0.5f - xOffsetNormalized; else xOffset = 0.5f + xOffsetNormalized; } return xOffset; } public synchronized void addMovingListener(ZTouchMoveListener listener) { mListeners.add(listener); } private synchronized void dispatchMoving() { Iterator<ZTouchMoveListener> iterator = mListeners.iterator(); while(iterator.hasNext()) { ((ZTouchMoveListener) iterator.next()).onTouchOffsetChanged(normalizePosition(mPosition + mPositionDelta)); } } } 

मैं तुरंत एक आरक्षण करना चाहता हूं कि कोड सुपर साफ और सुव्यवस्थित होने का दिखावा नहीं करता है, मेरे लिए यह महत्वपूर्ण था कि यह अपना काम पूरा करे, एक केश के लिए समय नहीं था।

ZTouchMove क्लास में एक onTouchEvent (MotionEvent e) मेथड है, एक इनपुट की तरह, जिसे WallpaperService.Engine क्लास के onTouchEvent से कॉल किया जाता है। इसके बाद, आपके रेंडरर को ZTouchMoveListener इंटरफ़ेस को लागू करना चाहिए, onTouchOffsetChanged (फ्लोट xOffset) पद्धति के साथ , जो सामान्य रूप से 0 से 1 तक परिणाम को स्वीकार करेगा।

यह भी आवश्यक है कि ZTouchMove को init (Context ctx) विधि कहकर, इसके संदर्भ में एप्लिकेशन को पास करके आरंभ किया जाए। स्क्रीन की चौड़ाई और कुछ अन्य मापदंडों को निर्धारित करने के लिए यह आवश्यक है। और रेंडरर को एक इवेंट श्रोता के रूप में भी पंजीकृत करें:
 mTouchMove = new ZTouchMove(); mTouchMove.init(ctx); mTouchMove.addMovingListener(mRenderer); 


चूंकि मुझे वर्चुअल डेस्कटॉप की संख्या निर्धारित करने का कोई तरीका नहीं मिला, इसलिए यह पैरामीटर चर mNumVirtualScreens में हार्डकोड किया गया था। यदि वांछित है, तो आप इसे बदलने के लिए एक विधि जोड़ सकते हैं और अपने विवेक पर इसका उपयोग कर सकते हैं।

ZTouchMove वर्ग के एनीमेशन और जड़ता के कार्यान्वयन की विशेषताएं: धीमी गति से आंदोलनों के दौरान, "जड़ता" को ट्रिगर किया जाता है, तेज आंदोलनों के दौरान, अगले आभासी डेस्कटॉप के लिए "करीब" ट्रिगर होता है। चरम स्थिति में, "वसंत" काम करता है।

इस पद्धति की कमियों के बीच, यह डेस्कटॉप और वॉलपेपर के आंदोलन के गैर-सिंक्रनाइज़ेशन को ध्यान देने योग्य है। यही है, ऐसा हो सकता है कि डेस्कटॉप पहले से ही चरम स्थिति में "आराम" कर चुका है, और वॉलपेपर अभी भी स्थानांतरित किया जा सकता है। या डेस्कटॉप पर, एक निश्चित गति से, आसन्न स्क्रीन के लिए "करीब" काम करेगा, और वॉलपेपर का "करीब" काम नहीं कर सकता है। इन प्रभावों को बाहर करना संभव नहीं है, क्योंकि सिद्धांत रूप में हमें डेस्कटॉप की वर्तमान स्थिति के बारे में जानकारी नहीं है।

हाइब्रिड समाधान


उपयोगकर्ता स्वयं सेटिंग्स में "लंबन" विधि का चयन करेगा, या आप स्वचालित रूप से यह निर्धारित कर सकते हैं कि मानक विधि काम करती है, और यदि नहीं, तो ZTouchMove पर स्विच करें। यहां स्वचालित पहचान का कार्यान्वयन है:

 if(xOffset != 0 && xOffset != 0.5f && xOffset != 1 || mOffsetChangedEnabled) { mOffsetChangedEnabled = true; mXPos = xOffset - 0.5f; //    setupLookatM(); } 


यह इस तथ्य पर आधारित है कि मानक कार्यान्वयन में xOffset 0, 0.5 और 1 के अलावा अन्य मानों को स्वीकार नहीं करता है, यदि मानक onSffsetsChanged विधि के WallpaperService.Engine वर्ग सही तरीके से काम नहीं करता है। तदनुसार, mOffsetChangedEnabled ध्वज डिफ़ॉल्ट रूप से गलत है, और इसका मतलब है कि ZTouchMove वर्ग को काम करना चाहिए।

व्यक्तिगत रूप से, मैंने एक हाइब्रिड सेटिंग को चुना, जहां डिफ़ॉल्ट रूप से स्वचालित पहचान काम करती है, और दो और विकल्प हैं: "डेस्कटॉप मोड" और "टच मोड"।

अद्यतन: दो कार्यान्वयन विधियों का एक वीडियो।

Source: https://habr.com/ru/post/In164367/


All Articles