Home  >  

Search Friends Component

Author photo
AddThis Social Bookmark Button
    Welcome back to the series. In the previous articles we discussed topics like publishing of news, sending notifications to specific friends etc. The articles covered topics that are not only bound to the user interface in the facebook application development. This time, we are going to play around with the already existing (but complex) MultiFriend component. The component from the article 11 was quite good, but it really had the potential to be a kick ass component for facebook. There were things I wanted to implement, but was not able to because the whole article covered the basic construction of the component.
In this article we will take the MultiFriendComponent and add functionality that will go beyond the basic stuff. Look at the original facebook search box:

article14_img1.png
As you can see beside the name of the friends, there are other fields that appear in the search box, like the network of which the friends is the member of. Also, in the case of the original facebook search box, when we type in the name, not only the names are searched, but also the surnames, this is a big key advantage in comparison to the component we made few weeks ago.
These are just one of the features we want to implement in the extended version of the component, however there is one thing to note. We will use inheritance, so this way we will not touch the code we already implemented, this is the power of inheritance! Readers who did not complete the article 11 will also be able to follow this article because of inheritance, they do not need to understand the code that was previously written, they will just rely on the power of it. How many benefits of OOP are there! :-) Enough theory, let's move on to the practical part.
The first step in this approach is to download the FLA that came provided with the 11th article. The download link is here. Let's open it to see what is inside and what we need to start. Here is the screen shot:

article14_img2.png
As we can see, there is the text field from the previous article that is ready and already working. We will not repeat ourselves here as we already know that the component works fine. The first goal will be to implement a finer search technique that will search the names and the surnames of friends. For that, we first need to look at the structure of the inheritance that will work in this case:

article14_img3.png
In this image above it is clearly visible how the inheritance structure looks like. At the top, there is the UIComponent that does all the hard work of drawing and resizing the component. Below, there is our custom MultiFriendField Component that is explained in article 11. Then finally, at the end, there is our SearchFriendField that will be created right now. So, open your favorite text editor and place the following class definition into it:


package facebookUI {
 
 	public class SearchFriendsField extends MultiFriendField{
  
  		function SearchFriendsField(){
   			trace(this + „ works...“);
   		}
  
  	}
 
}
Let's see how this works. In order for this to work, we need to make sure the the newly created class is linked to the MovieClip we want here. Let's change the linkage:

article14_img4.png
So, now it's turn for the big test. We need to check if the whole inheritance idea works for us. Let's compile the swf and run it inside the facebook application canvas. Here is the result on my side:

article14_img5.png
Cool, this is the sign that the inheritance in the class is working! We have all the hard work from article 11 in one class and we have all the place in the class SearchFriendsField to add new features.
Now we talked about that how to implement a new search functionality. We need a way to search for names and surnames. In order to update the search, we need to find the method that does the search, and that is:


protected function textChanged(e:Event):void {
 
 	var names:Array = _inputField.text.split(",");
 	var lastName:String = names[names.length - 1];
 	_suggestClip.suggest(trim(lastName));
 
}
Currently, this method only passes the names of the users, we need to do some magic to implement the search functionality for the names and the surnames. The line of code:

var lastName:String = names[names.length - 1];
is a bit misleading (my fault :-) ). It passes whole name, like "Mirza Hatipovic" instead of "Hatipovic" or only "Mirza". The whole method need to look like this:

override protected function textChanged(e:Event):void {
 
 			var names:Array = _inputField.text.split(",");
 			var name:String = names[names.length - 1];
 			var n:Array = name.split(" ");
 			var firstName:String = n[0];
 			var lastName:String = n[1];
 
 			_suggestClip.suggest(trim(firstName));
 
 			if(lastName){
  				_suggestClip.suggest(trim(lastName));
  			}
 
}
Unfortunately it is not enough, we need to open SuggestClip.as and modify the method getSimilarWords():


private function getSimilarWords(typed:String){
 
 	this.visible = true;
 	_wordSugg.clearAll();
 	var w = _words;
 	var i = 0;
 	var len = w.length;
 	for(i = 0; i < len; i++){
  
  		var currword:String = w[i];
  
  		var n:Array = currword.split(" ");
  		var firstName:String = n[0];
  		var lastName:String = n[1];
  
  		if(typed.toLowerCase() == (firstName.substr(0, typed.length).toLowerCase())){
   			_wordSugg.addWord(currword, typed.length);
   		}
  
  		if(typed.toLowerCase() == (lastName.substr(0, typed.length).toLowerCase())){
   			_wordSugg.addWord(currword, typed.length);
   		}
  
  	}
 
 	if(i > 0){
  		_wordSugg.focusFirst();
  	}
 
}
And finally we see in the image that the component is seaching both for names and surnames 

article14_img6.png
Cool, we finally have the right algorithm for friends search. The search is more complex and a lot more precise then the previous one.
The next thing we need to do is to display the country / network the friend belongs to. Here is the image to remind how this looks like in reality:

article14_img1.png
As we can see, it does not show up below every friend, but it does show up below friends that entered such information. In order to achieve this, we need to tweak not just the code, but also the MovieClip that holds the information. So, let's search for this MovieClip inside the library. On my side the clip is named WordButton.

article14_img7.png
So, let's open it to see how it looks like inside before we make any tweaks to the clip:

article14_img8.png
Hey, inside the clip there is only a text field to display the name properly. We need to create a new one below it, font size 7 and font color light gray.

article14_img9.png
Now the clip is ready and it can be used in the script. It would be nice to start the application in the current state to see how the new text field looks like:

article14_img10.png
Ok, we see that the new text field appears right below the name and that was the desired result. The next thing we need to do is to populate the newly created text field with the network name. Hmm, let's examine how to do that. The method Users.getInfo() had fields that gives us the geographical location:

user -> current_location -> country
That's cool, now we need to check where we update the information to make the necessary modifications. Open MultiFriendField.as and find the following method:


protected function onDataRecieve(e:FacebookEvent){
  	
 			var userData = ((e.data as GetInfoData).userCollection);
 			
 			var i;
 			var len = userData.length;
 		 
 			for(i = 0; i < len; i++){
  				var user = userData.getItemAt(i);
  				trace(user.first_name + " " + user.last_name);
  				_suggestClip.addWord(user.first_name + " " + user.last_name);
  			}
  	
}
The same method will be overridden in the SearchFriendField method. It looks like this:
 
override protected function onDataRecieve(e:FacebookEvent){
  	
 			var userData = ((e.data as GetInfoData).userCollection);
 			
 			var i;
 			var len = userData.length;
 		 
 			for(i = 0; i < len; i++){
  				var user = userData.getItemAt(i);
  				_suggestClip.addWord(user.first_name + " " + user.last_name + " " + user.current_location.country);
  			}
  	
 		}
As a result we see the country in the output window but still we did not display it. Here is how we can do it. We need to add the country to the getSimilarWords method, provided by SuggestClip.as:
SuggestClip.as:


private function getSimilarWords(typed:String){
 			this.visible = true;
 			_wordSugg.clearAll();
 			var w = _words;
 			var i = 0;
 			var len = w.length;
 			for(i = 0; i < len; i++){
  
  				var currword:String = w[i];
  
  				var n:Array = currword.split(" ");
  				var firstName:String = n[0];
  				var lastName:String = n[1];
  				var country:String = n[2];
  
  				if(typed.toLowerCase() == (firstName.substr(0, typed.length).toLowerCase())){
   					_wordSugg.addWord(currword, typed.length, country);
   				}
  
  				if(typed.toLowerCase() == (lastName.substr(0, typed.length).toLowerCase())){
   					_wordSugg.addWord(currword, typed.length, country);
   				}
  
  
  			}
 			if(i > 0){
  				_wordSugg.focusFirst();
  			}
 		}


In SuggWord.as:

public function addWord(t:String, len:Number, country:String):void{
 			
 			_clipCounter++;
 			
 			var word_mc:MovieClip = new WordButton();
 			word_mc.name = "field" + _clipCounter;
 
 			_clips["field" + _clipCounter] = word_mc;
 
 			this.addChild(word_mc);
 			word_mc.setWord(t, len, country);
 
 			var clip = _clips["field" + (_clipCounter - 1)];
 			var prevClipExists = Boolean(clip);
 
 			if(prevClipExists){
  				word_mc.y = clip.y + clip.height;
  			} else {
  				word_mc.y = 1;	
  			}
 
 
 			word_mc.addEventListener(MouseEvent.CLICK, wordClicked);
 			
 		}

<div style="text-align: left;">In WordButton.as:</div>

public function setWord(w:String, len:Number, c:String):void{
 			main_txt.visible = false;
 			main_txt.htmlText = "<b>" + w.substr(0, len) + "</b>";
 			var wid:Number = main_txt.textWidth;
 			main_txt.htmlText = "<b>" + w.substr(0, len) + "</b>" + w.substr(len, w.length);
 			main_txt.visible = true;
 			textBG_mc.width = wid;
 			network_txt.text = c;
 			_word = w;
 		}
After those modifications are done (it might be a pain, sorry), we can see in the image below that the country was set:

article14_img11.png
Now I agree that this was a bit complicated and hard to follow. We have added two cool features that make the component much much more useful. Especially the new search feature looks more like the original facebook friend search.
We already worked on fonts. It would be neat if we could implement a feature where the user can use different fonts that fit into the environment of the application. For that we can apply a special fontFace property at the end of the SearchFriendField class. It looks like this:

public function fontFace(e:String){
 	_fontFace = e;
 	invalidate();
}
We simply set the font and after that we cann the invalidate method that will redraw the component. The draw() method is located in the MultiFriendField class and looks like this:

override protected function draw():void {
 
 	// always call super.draw() at the end
 	super.draw();
 
}
This is how it looks like inside the MultiFriendField.as component. We will write the same inside the SearchFriendField.as class:


override protected function draw():void {
 
 	// always call super.draw() at the end
 	super.draw();
 
 	//do additional stuff here...
 
 
}
Now, as soon as invalidate() is called, the following flow occurs:

article14_img12.png
So, let's concentrate on the draw() function inside the SearchFriendsField() class. Here is the flow of how the methods get called inside the component:

article14_img13.png
Looks a bit complicated but actually it's not. In the draw function, the following happens:

override protected function draw():void {
 
 	// always call super.draw() at the end
 	super.draw();
 
 	//set new font face
 	suggestClip.fontFace = _fontFace;
 
}
Looking at the image above, we see that the next clip that needs to be called is SuggWord. And here is how this looks like inside SuggestClip:


public function fontFace(e:String){
 	_fontFace = e;
}
Please dont get confused, hold on! One thing we must consider here. We need to assign the fontFace to every WordButton and this will be done in the loop that creates the array of WordButtons:


public function addWord(t:String, len:Number, country:String):void{
 			
 	_clipCounter++;
 			
 	var word_mc:MovieClip = new WordButton();
 	word_mc.name = "field" + _clipCounter;
 
 	word_mc.fontFace = _fontFace;
 	...
So, as we can see it in the image, every time the new WordButton is added the the display list, the new fontFace is assigned. Looking at the item here, we need to open WordButton as and add the same faceFont setter:


public function fontFace(e:String){
 	_fontFace = e;
}
Now let's see it this actually works:

article14_img14.png
Finally we got it. As we can see the complete architecture of the component is pretty complex it's not very easy to tweak things that live inside it.
In order not to confuse you, here are the codes of all the classes used:


package facebookUI {
 
 	import flash.events.*;
 	import com.facebook.events.FacebookEvent;
 	import com.facebook.data.users.GetInfoData;
 	import com.facebook.data.FacebookNetwork;
 
 	public class SearchFriendsField extends MultiFriendField{
  
  		protected var _fontFace:String = "Arial";
  
  		function SearchFriendsField(){
   			trace(this + " works...");
   		}
  
  		override protected function draw():void {
   
   			// always call super.draw() at the end
   			super.draw();
   
   			_suggestClip.fontFace = _fontFace;
   
   		}
  
  		
  		override protected function textChanged(e:Event):void {
   
   			var names:Array = _inputField.text.split(",");
   			var name:String = names[names.length - 1];
   			var n:Array = name.split(" ");
   			var firstName:String = n[0];
   			var lastName:String = n[1];
   
   			_suggestClip.suggest(trim(firstName));
   
   			if(lastName){
    				_suggestClip.suggest(trim(lastName));
    			}
   
   		}
  
  		
  		override protected function onDataRecieve(e:FacebookEvent){
    	
   			var userData = ((e.data as GetInfoData).userCollection);
   			
   			var i;
   			var len = userData.length;
   		 
   			for(i = 0; i < len; i++){
    				var user = userData.getItemAt(i);
    				_suggestClip.addWord(user.first_name + " " + user.last_name + " " + user.current_location.country);
    			}
    	
   		}
  
  		public function set fontFace(e:String):void {
   			_fontFace = e;
   			invalidate();
   		}
  
  	}
 
}
SuggestClip.as:


package facebookUI{
 
 	import flash.display.*;
 	import flash.text.*;
 	import flash.events.*;
 
 	public class SuggestClip extends SnapClip{
  
  		private var _words:Array = null;
  		private var _wordSugg:MovieClip = new SuggWord();
  
  		protected var _fontFace:String = "Arial";
  		
  		function SuggestClip(){
   			super();
   			this.visible = false;
   			_words = new Array();
   			this.addChild(_wordSugg);
   		}
  
  
  
  		private function keyPressed(e:KeyboardEvent):void {
   
   			if(e.keyCode == 40){
    				_wordSugg.focusNext();
    			}
   
   			if(e.keyCode == 38){
    				_wordSugg.focusPrev();
    			}
   
   			if(e.keyCode == 13){
    				var word:String = _wordSugg.getWord();
    				var ex:SuperEvent = new SuperEvent("wordClicked", true, true);
    				ex.add("word", word);
    				dispatchEvent(ex);
    				this.visible = false;
    			}
   
   		}
  
  		public function activate():void{
   			stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed);
   		}
  
  		public function deactivate():void{
   			this.removeEventListener(KeyboardEvent.KEY_DOWN, keyPressed);
   		}
  
  		private function getSimilarWords(typed:String){
   			
   			this.visible = true;
   			
   			_wordSugg.clearAll();
   			var w = _words;
   			var i = 0;
   			var len = w.length;
   
   			for(i = 0; i < len; i++){
    
    				var currword:String = w[i];
    
    				var n:Array = currword.split(" ");
    				var firstName:String = n[0];
    				var lastName:String = n[1];
    				var country:String = n[2];
    
    				if(typed.toLowerCase() == (firstName.substr(0, typed.length).toLowerCase())){
     					_wordSugg.addWord(n[0] + n[1], typed.length, country);
     				}
    
    				if(typed.toLowerCase() == (lastName.substr(0, typed.length).toLowerCase())){
     					_wordSugg.addWord(n[0] + n[1], typed.length, country);
     				}
    
    
    			}
   			if(i > 0){
    				_wordSugg.focusFirst();
    			}
   		}
  		
  		public function addWord(w:String):void{
   			_words.push(w);
   		}
  		
  		public function suggest(typed:String):void{
   			
   			if(typed == ""){
    				_wordSugg.clearAll();
    				return;
    			}
   		
   			var similarWords = getSimilarWords(typed);
   
   		}
  
  		public function set fontFace(e:String):void {
   			_fontFace = e;
   			_wordSugg.fontFace = e;
   		}
  		
  	}
 
}
SuggWord.as:

package facebookUI{
 
 	import flash.display.*;
 	import flash.events.*;
 
 	public class SuggWord extends MovieClip{
  		
  		private var _focusIndex:Number = 0;
  		private var _currentWord:String = "";
  		private var _clipCounter:Number = 0;
  		private var _clips:Object = new Object();
  
  		protected var _fontFace:String = "Arial";
  		
  		function SuggWord(){
   			super();
   		}
  
  		public function clearAll():void {
   			while (this.numChildren){
    				this.removeChildAt(0);
    			}
   
   
   
   			_clips = new Object();
   			_clipCounter = 0;
   			_focusIndex = 0;
   		}
  		
  		public function wordClicked(e:MouseEvent){
   
   			var clip = e.currentTarget;
   			dispatchWord(clip.getWord());
   			_focusIndex = 0;
   
   		}
  		
  		private function dispatchWord(word:String){
   			var e:SuperEvent = new SuperEvent("wordClicked", true, true);
   			e.add("word", word);
   			dispatchEvent(e);
   		}
  		
  		public function addWord(t:String, len:Number, country:String):void{
   			
   			_clipCounter++;
   			
   			var word_mc:MovieClip = new WordButton();
   			word_mc.name = "field" + _clipCounter;
   
   			word_mc.fontFace = _fontFace;
   
   			_clips["field" + _clipCounter] = word_mc;
   
   			this.addChild(word_mc);
   			word_mc.setWord(t, len, country);
   
   			var clip = _clips["field" + (_clipCounter - 1)];
   			var prevClipExists = Boolean(clip);
   
   			if(prevClipExists){
    				word_mc.y = clip.y + clip.height;
    			} else {
    				word_mc.y = 1;	
    			}
   
   			word_mc.addEventListener(MouseEvent.CLICK, wordClicked);
   			
   		}
  		
  		public function focusPrev():void{
   
   			if(_focusIndex == 0){
    				return;
    			}
   
   			_focusIndex--;
   	
   			var clip = _clips["field" + _focusIndex];
   			var clipExists = Boolean(clip);
   
   			if(clipExists){
    
    				clip.rolledOver(new MouseEvent(MouseEvent.ROLL_OVER));
    				_currentWord = clip.getWord();
    
    				var prevClip = _clips["field" + (_focusIndex + 1)];
    				var prevClipExists = Boolean(prevClip);
    
    				if(prevClipExists){
     					prevClip.rolledOut(new MouseEvent(MouseEvent.ROLL_OUT));
     				}
    
    
    
    				
    
    			}
   		}
  		
  		public function focusNext():void{
   			
   			if(_focusIndex == (_clipCounter + 1)){
    				return;
    			}
   
   			if(_focusIndex == 0){
    				focusFirst();
    			}
   				
   			var clip = _clips["field" + _focusIndex];
   			var clipExists = Boolean(clip);
   
   			if(clipExists){
    
    
    
    				clip.rolledOver(new MouseEvent(MouseEvent.ROLL_OVER));
    				_currentWord = clip.getWord();
    
    				var prevClip = _clips["field" + (_focusIndex - 1)];
    				var prevClipExists = Boolean(prevClip);
    
    				if(prevClipExists){
     					prevClip.rolledOut(new MouseEvent(MouseEvent.ROLL_OUT));
     				}
    
    				_focusIndex++;
    
    			}
   
   		}
  		
  		public function focusFirst():void{
   
   			_focusIndex = 1;
   			var clip = _clips["field" + _focusIndex];
   
   			var clipExists = Boolean(clip);
   			if(clipExists){
    				clip.rolledOver(new MouseEvent(MouseEvent.ROLL_OVER));
    				_currentWord = clip.getWord();
    			}
   
   
   
   		}
  		
  		public function getWord():String{
   			return _currentWord;
   		}
  
  		public function set fontFace(e:String):void {
   			_fontFace = e;
   		}
  
  	}
 
}
WordButton.as:


package facebookUI{
 
 	import flash.display.*;
 	import flash.events.*;
 	import flash.text.*;
 
 	public class WordButton extends MovieClip{
  		
  		private var _word:String = null;
  
  		private var _tf = new TextFormat();
  		
  		function WordButton(){
   			super();
   			stop();
   			init();
   		}
  		
  		private function init():void{
   			this.buttonMode = false;
   			textBG_mc.width = 0;
   			this.addEventListener(MouseEvent.ROLL_OVER, rolledOver);
   			this.addEventListener(MouseEvent.ROLL_OUT, rolledOut);
   			main_txt.defaultTextFormat = _tf;
   			network_txt.defaultTextFormat = _tf;
   			main_txt.setTextFormat(_tf);
   			network_txt.setTextFormat(_tf);
   		}
  
  		public function rolledOver(e:MouseEvent):void {
   			_tf.color = 0xffffff;
   			main_txt.defaultTextFormat = _tf;
   			network_txt.defaultTextFormat = _tf;
   			main_txt.setTextFormat(_tf);
   			network_txt.setTextFormat(_tf);
   			this.gotoAndStop(2);
   		}
  
  		public function rolledOut(e:MouseEvent):void {
   			_tf.color = 0x000000;
   			main_txt.defaultTextFormat = _tf;
   			network_txt.defaultTextFormat = _tf;
   			main_txt.setTextFormat(_tf);
   			network_txt.setTextFormat(_tf);
   			this.gotoAndStop(1);
   		}
  		
  		public function getWord():String{
   			return _word;
   		}
  
  		public function setWord(w:String, len:Number, c:String):void{
   			main_txt.visible = false;
   			main_txt.htmlText = "<b>" + w.substr(0, len) + "</b>";
   			var wid:Number = main_txt.textWidth;
   			main_txt.htmlText = "<b>" + w.substr(0, len) + "</b>" + w.substr(len, w.length);
   			main_txt.visible = true;
   			textBG_mc.width = wid;
   			network_txt.text = c;
   			_word = w;
   		}
  
  		public function set fontFace(e:String):void {
   			_tf.font = e;
   		}
  
  
  
  	}
 
}


To view the entire series please visit http://www.insideria.com/series-facebook-dev.html

Read more from Mirza Hatipovic. Mirza Hatipovic's Atom feed mirzahat on Twitter

Comments

3 Comments

RIA Mad said:

Really Great one..

Fun said:

Great! reaaly very helpful

could you post the SuperEvent.as code.

Without that we can't run the sample :(

brew norman said:

another tutorial that DOESN'T WORK!!! wtf?

"The first step in this approach is to download the FLA that came provided with the 11th article."
please let me know how to get this file to work. i downloaded it, opened it in cs4 and i get these errors. pretty lazy if you cant even get your own files provided to run!

google Emanuele Feronato , her tutorials work!!!

1017: The definition of base class UIComponent was not found.
1020: Method marked override must override another method.
5000: The class 'facebookUI.SearchFriendsField' must subclass 'flash.display.MovieClip' since it is linked to a library symbol of that type.
5000: The class 'FacebookShim' must subclass 'flash.display.MovieClip' since it is linked to a library symbol of that type.
5000: The class 'facebookUI.WordButton' must subclass 'flash.display.MovieClip' since it is linked to a library symbol of that type.
5000: The class 'facebookUI.SuggestClip' must subclass 'flash.display.MovieClip' since it is linked to a library symbol of that type.

Leave a comment


Tag Cloud

Poll: Mobile Features

What feature do you use most on your mobile phone?

Vote | View Poll Results | Read Related Blog Entry

Latest Features

Recommended for You

@InsideRIA on Twitter

Archives

  • Or, visit our complete archive.  

About This Site

Welcome to the premiere community site for all things RIA sponsored by O'Reilly Media and Adobe Systems Incorporated.